# The network capability, in full

Hand-written, not generated. Everything here is a decision or a shape that changes rarely; the live inventory of adapter packs is in leumas-capabilities/reference/adapters.md.


Hand-written, not generated. Everything here is a decision or a shape that changes rarely; the live
inventory of adapter packs is in `leumas-capabilities/reference/adapters.md`.

## Where the code is

| Piece | Path |
|---|---|
| The SDK | `shared/engines/network/` — `@leumas/network-sdk` |
| Browser-safe half | `shared/engines/network/src/client.js` — pure modules only, no node builtins |
| Detection | `shared/engines/network/src/detect.js` |
| Scan policy | `shared/engines/network/src/targets.js` |
| Nmap provider | `shared/engines/network/src/providers/nmap/` — `args.js` · `parse.js` · `index.js` |
| TShark provider | `shared/engines/network/src/providers/tshark/` — `args.js` · `parse.js` · `filters.js` · `index.js` |
| What a device *is* | `shared/engines/network/src/normalize/devices.js` |
| What a device is *called* | `shared/engines/network/src/normalize/present.js` |
| Traffic rollup | `shared/engines/network/src/normalize/rollup.js` |
| Snapshot comparison | `shared/engines/network/src/normalize/diff.js` |
| Default route | `shared/engines/network/src/gateway.js` |
| Running work | `shared/engines/network/src/jobs.js` |
| Streaming routes | `shared/engines/network/src/router.js` |
| Adapter pack | `shared/engines/adapters/domain/network/` |
| API mount | `products/leumas-api/src/planes/appliance.js` → `mountNetworkTools` |
| Studio surface | `products/leumas-studio/src/admin/information/network/` |
| Tab registration | `products/leumas-studio/src/admin/information/model/tabs.js` |
| Smoke test | `ops/infra/scripts/smoke-network.mjs` |

## Verbs

Every one resolves; none throws for an expected outcome.

| Verb | Does |
|---|---|
| `health({ refresh })` | which binaries exist, their versions, paths, and an install guide for the running platform |
| `localNetworks({ listAdapters })` | the subnets this machine is on; `primary` is what "my network" means |
| `defaultGateway()` | the address traffic leaves through — the one device nameable as a fact |
| `discover({ target })` | what is on a network. No port is touched |
| `inspect({ target })` | one device's common ports |
| `services({ target })` | what software answers on those ports |
| `inventory({ target, hostLimit })` | discover, then inspect everything found, in one nmap run |
| `interfaces({ listAdapters })` | capture adapters, enriched with friendly names and addresses |
| `capture({ interfaceIndex, … })` | a bounded live capture, streaming summaries |
| `observe({ address })` | a capture filtered to one device |
| `analyze({ file })` | a saved pcap, through the same normalizer as a live capture |
| `validateFilter({ interfaceIndex, filter })` | check a BPF filter without capturing |
| `snapshot(scan)` / `diff(before, after)` | the small history record, and what changed |

The adapter pack exposes the same set at `/api/adapters/network/<verb>`, as MCP tools, and as AI
function-calls — one file on disk, three surfaces, no router written.

## Routes

`/api/network`, machine-local planes only, `requireAuth` + `toolLimit`.

`GET /health` · `GET /networks` · `GET /interfaces` · `POST /filter/validate` · `POST /diff` ·
`POST /scan` · `POST /capture` · `POST /analyze` · `GET /stream?job=` (SSE) · `GET /jobs` ·
`GET /jobs/:id` · `POST /jobs/:id/cancel` · `DELETE /jobs/:id` · `GET /jobs/:id/file`

Long work returns `202 { jobId }` and reports over SSE. Every job is scoped to its owner; somebody
else's job is reported **absent**, not forbidden, so the endpoint cannot be used to learn it exists.

## Limits

All of them in `shared/engines/network/src/limits.js` — never a number typed into a component.

Capture defaults 30s · 50,000 packets · 100 MB, whichever comes first. Ceilings 300s · 500,000 · 512 MB.
Scans default to a 120s budget and a 256-host limit, ceiling 1024. A pcap over 512 MB is refused by
size before tshark is spawned.

The Nmap tab sweeps automatically on arrival only when the detected network is at or under
`AUTO_MAX_HOSTS` (1024 addresses) — anything larger renders the same page with a button and the count
named. Results are cached for five minutes across mounts, because the tab remounts on every tab
switch and StrictMode mounts it twice.

## Scan profiles

| Profile | Arguments |
|---|---|
| `discover` | `-sn -T4 --stats-every 1s -oX -` |
| `inspect` | `-sT -F -T4 --host-timeout 60s --stats-every 1s -oX -` |
| `services` | `-sT -sV --version-intensity 2 -F -T4 -oX -` |
| `inventory` | `discover`, then one `inspect` over every address found |

`-sT` (connect) rather than `-sS` (SYN) throughout, because connect needs no privileges and an
operator who is not an administrator is the common case. `-O` and `-sS` are offered only when
`health().nmap.privileged` is true, and are dropped rather than failing when it is not.

## Capture and analysis arguments

Live: `-i N [-f BPF] -a duration:S -a filesize:KB -c N -w FILE -P -T ek -q -l`. All three bounds are
always set — any one alone is a capture that ends when something else runs out. `-w` plus `-P`
writes the pcap *and* prints summaries, which is what makes "save this capture" possible at all.

File: `-r FILE [-Y display-filter] -c N -T ek -e …` with the fields named explicitly, because
without `-w` tshark is not in write-and-print mode.

Filter validation: `dumpcap -i N -d -f FILTER` compiles and exits. Read the message, not the code.

## The two filter languages

They look alike and are not the same, which is the most common thing to get wrong:

- **Capture filter** (BPF, `-f`) decides what is **recorded**: `tcp port 443`. What it drops is gone.
- **Display filter** (`-Y`) decides what is **shown** from what was already recorded: `tcp.port == 443`.

`quickCheckFilter(text, kind)` catches the mix-up instantly and by name, which is far more use than
handing either one to a parser that will answer "syntax error".

## Device identity

`classifyDevice` scores vendor OUI, open ports and OS-detection evidence into
`confident` (≥4) / `likely` (≥2) / `unknown`, and renders a matching label. A randomised MAC
short-circuits vendor matching entirely — phones rotate their hardware address per network, so
reading a vendor off one produces confident nonsense.

`presentDevice` then turns that into words: ISP suffixes stripped, UUID and MAC-shaped hostnames
discarded, a consumer noun from the vendor when there is no usable name. It may not change a
confidence. `groupDevices` sorts into fixed sections — the router, this computer, what we recognised,
what we could not — because the order *is* the sort for somebody who cannot sort a table.

## Where results come from

nmap ships the full IEEE OUI database and names most consumer hardware. `oui.js` in
`@leumas/devices` knows a short hand-seeded list and is not a substitute. `arpNeighbours()` is fully
passive but reads a sparse OS cache — measured at a fraction of what a sweep finds. If you want
identity, run the sweep.


---
Source: .claude/skills/leumas-network/reference/capabilities.md
Canonical: https://docs.leumas.tech/p/skills/leumas-network/capabilities
