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.
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.
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.