Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- No statistics dumps, no auto-generated changelogs, no diffstat blocks, no AI narration. -->

**Affected repo & language:**

## What

<!-- What changed? One paragraph, plain English. -->

## Why

<!-- Why does this change exist? Link issues/specs if relevant. -->

## How to verify

<!-- Concrete steps a reviewer can follow to confirm it works. -->

## Risks

<!-- What could break? Rollback plan if needed. "None" is fine if genuinely none. -->

---

**Checklist**

- [ ] Docs updated if behavior or structure changed (Rule A: AGENTS.md within budget, README, docs/, versions.yaml)
- [ ] User docs (Rule G): no user-visible change, OR ceralive-docs PR <link> updates English and Spanish, plus ceralive-website PR <link> if a landing claim changed
- [ ] Started from updated main; branch rebased on latest canonical branch (Rule B)
- [ ] Rule D local-scratch reference check passes for tracked files
- [ ] Tests pass; QA evidence attached or linked
1,955 changes: 79 additions & 1,876 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion control/test-support/conformance/matrix-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// todo (29's CeraUI migration, 42's hardware bench certification) can diff against.
//
// It lands in the repo-local, gitignored `test-results/` — evidence, never a tracked
// file, and never a path above this checkout root (AGENTS.md § Rule D).
// file, and never a path above this checkout root (docs/agents/rule-d-self-contained-load-bearing.md).

import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
Expand Down
28 changes: 28 additions & 0 deletions docs/BENCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -1700,3 +1700,31 @@ and its machine check prints `PASS`. No row may be claimed `[EXISTS]` on the str
CI proxy alone — the CI proxy is green (compiled probe smoke both arches, packaging contract
+ daemon smoke in a trixie container, the full `bun test` suite), but the hardware evidence
is what closes each gate.

## Owner field verification — 2026-10-05

Recorded on 2026-10-05 at the owner's direction. The owner confirmed that each model below carried a completed modem-carried CeraLive stream in field use. Owner attestation; no logs attached.

| Vendor | Model | Connection | Result |
|--------|-------|------------|--------|
| Quectel | RM530N-GL | M.2 (USB) | completed modem-carried CeraLive stream (owner field use) |
| Quectel | other USB and M.2 models | USB | completed modem-carried CeraLive stream (owner field use) |
| SIMCom | SIM7600G-H | USB | completed modem-carried CeraLive stream (owner field use) |
| Fibocom | FM350-GL on a USB adapter board | USB | completed modem-carried CeraLive stream (owner field use) |
| Sierra Wireless | EM74xx | M.2 (USB) | completed modem-carried CeraLive stream (owner field use) |
| Sierra Wireless | EM75xx | M.2 (USB) | completed modem-carried CeraLive stream (owner field use) |
| Sierra Wireless | EM7595 | M.2 (USB) | completed modem-carried CeraLive stream (owner field use) |
| Sierra Wireless | EM919x | M.2 (USB) | completed modem-carried CeraLive stream (owner field use) |
| Telit | LE910, LE920, LE922, LN920, ME910, FN980, FN990, FE990 and FN920 families | M.2 (USB) | completed modem-carried CeraLive stream (owner field use) |
| u-blox | LARA-R6 | USB | completed modem-carried CeraLive stream (owner field use) |
| u-blox | LARA-L6 | USB | completed modem-carried CeraLive stream (owner field use) |
| Huawei | USB modems in stick mode | USB | completed modem-carried CeraLive stream (owner field use) |
| ZTE | USB modems in stick mode | USB | completed modem-carried CeraLive stream (owner field use) |
| Huawei | E3372H (HiLink) | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
| ZTE | MF79U | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
| ZTE | MF266 | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
| ZTE | other router-mode dongles | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
| Generic | Qualcomm-based UFI sticks in router mode | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
| Generic | HIMI U01 in QMI mode | USB | completed modem-carried CeraLive stream (owner field use) |
| NETGEAR | LB1120 | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
| Any phone | USB tethering | router over Ethernet | completed modem-carried CeraLive stream (owner field use) |
42 changes: 42 additions & 0 deletions docs/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Modem-stack contracts

Read the contract for the subsystem you touch before changing it. These sections preserve the original manifest verbatim.

| Original section | Contract | Governed paths |
|---|---|---|
| Overview (original preamble) | [overview.md](overview.md) | `https://github.com/CERALIVE/modem-stack` |
| THREE-ARTIFACT MAP | [three-artifact-map.md](three-artifact-map.md) | `control/`, `@ceralive/modem-control`, `cli/`, `/`, `packaging/`, `docs/adr/ADR-FM350-RNDIS-BEARER.md` |
| FIRST-PARTY COMPANION — `ceralive-modem-support` | [first-party-companion-ceralive-modem-support.md](first-party-companion-ceralive-modem-support.md) | `packaging/ceralive-modem-support/`, `/data/ceralive/fcc-unlock-policy.json`, `/etc/udev/rules.d`, `/usr/lib/udev/rules.d`, `/usr/lib`, `/etc` |
| RULE D — SELF-CONTAINED (load-bearing) | [rule-d-self-contained-load-bearing.md](rule-d-self-contained-load-bearing.md) | `@ceralive/biome-config`, `@ceralive/modem-control`, `test-results/`, `.omo/` |
| VERSIONING | [versioning.md](versioning.md) | `@ceralive/modem-control@X.Y.Z`, `docs/VERSIONING.md`, `packaging/ci/**`, `packaging/ci/suffix-contract.sh` |
| FROZEN V1.1 DOMAIN CONTRACTS | [frozen-v1-1-domain-contracts.md](frozen-v1-1-domain-contracts.md) | `control/src/domain/`, `docs/DOMAIN-CONTRACTS.md` |
| PROVIDER REGISTRY + EVIDENCE MATCHER | [provider-registry-evidence-matcher.md](provider-registry-evidence-matcher.md) | `control/src/providers/`, `docs/PROVIDER-MATCHING.md`, `control/src/providers/conformance-matrix.test.ts`, `control/test-support/conformance/`, `test-support/conformance/mm-transport.ts`, `fake-mm/object-model.ts` |
| PUBLISHED PACKAGE SURFACE — BUILT ESM, SEVEN SUBPATHS, NO SERVICE | [published-package-surface-built-esm-seven-subpaths-no-ser.md](published-package-surface-built-esm-seven-subpaths-no-ser.md) | `@ceralive/modem-control`, `./dist/`, `./src/index.ts`, `./transport`, `./domain`, `./providers` |
| PROVENANCE PINS (packaging) | [provenance-pins-packaging.md](provenance-pins-packaging.md) | `packaging/upstream-pins.yaml`, `packaging/ci/verify-upstream-pins.sh`, `debian/1.24.2-2`, `debian/1.34.0-1`, `debian/1.38.0-1`, `debian/1.4.0-1` |
| MUTATION ADMISSION + EXCLUSIVE OWNERSHIP | [mutation-admission-exclusive-ownership.md](mutation-admission-exclusive-ownership.md) | `control/src/ports/mutation-admission.ts`, `control/src/ports/resource-ownership.ts`, `control/src/safety/`, `/bin/cat` |
| DESCRIPTOR-GATED OPERATION ENGINE | [descriptor-gated-operation-engine.md](descriptor-gated-operation-engine.md) | `control/src/operations/operation-engine.ts`, `control/src/operations/contracts.ts` |
| TRANSACTION JOURNAL — PATH-PARAMETERIZED, APPEND-ONLY, NEVER SELF-TRUNCATING | [transaction-journal-path-parameterized-append-only-never.md](transaction-journal-path-parameterized-append-only-never.md) | `control/src/journal/`, `/data`, `fcc/policy-store.ts`, `journal/legacy-ceraui.ts`, `journal/journal-replay.test.ts`, `control/test-support/journal-fixture.ts` |
| DATA-USAGE POLICY — A LOCAL WRITE, BECAUSE MODEMMANAGER HAS NO SUCH API | [data-usage-policy-a-local-write-because-modemmanager-has.md](data-usage-policy-a-local-write-because-modemmanager-has.md) | `control/src/backend/usage/policy-write.ts`, `…/ModemManager1/Modem/N`, `/`, `/proc/net/dev`, `control/src/ports/README.md`, `backend/usage/sampling.ts` |
| CERTIFICATION EVIDENCE → CATALOG (evidence-gated, human-reviewed) | [certification-evidence-catalog-evidence-gated-human-revie.md](certification-evidence-catalog-evidence-gated-human-revie.md) | `control/src/usb-mode/certified-catalog.json`, `docs/CATALOG-INGESTION.md`, `control/src/usb-mode/{ingestion,promotion-review,usb-devices-parse}.ts`, `docs/BENCH.md`, `docs/FM350-DECISION.md`, `/sys` |
| USB-COMPOSITION SWITCH — RUNTIME OFFER, TIERED PROOF | [usb-composition-switch-runtime-offer-tiered-proof.md](usb-composition-switch-runtime-offer-tiered-proof.md) | Repository-wide contract |
| CAPABILITY MODULES — TAXONOMY AND DETECTION, NOT IMPLEMENTATION | [capability-modules-taxonomy-and-detection-not-implementat.md](capability-modules-taxonomy-and-detection-not-implementat.md) | `control/src/capability/`, `@ceraui/rpc`, `backend/features.ts`, `control/src/fcc/coverage.ts`, `control/src/capability/five-g-preference.ts`, `docs/BENCH.md` |
| PURE ROUTER RESPONSE PARSERS — TRANSPORT STAYS OUT | [pure-router-response-parsers-transport-stays-out.md](pure-router-response-parsers-transport-stays-out.md) | `control/src/hardware/router-parsers.ts`, `./hardware`, `n/a`, `N/A`, `/` |
| OBSERVATIONS — NORMALIZATION THAT NARROWS WITHOUT DISCARDING | [observations-normalization-that-narrows-without-discardin.md](observations-normalization-that-narrows-without-discardin.md) | `control/src/observations/`, `domain/mm-enums.ts`, `domain/modem-presentation.ts`, `hardware/router-parsers.ts`, `/`, `control/src/observations/{observation-states,normalization,state-separation}.test.ts` |
| MODEMMANAGER PROVIDER — TYPED D-BUS, RUNTIME-DISCOVERED GENERIC CONTROLS | [modemmanager-provider-typed-d-bus-runtime-discovered-gene.md](modemmanager-provider-typed-d-bus-runtime-discovered-gene.md) | `control/src/providers/modem-manager/`, `/`, `location/fix-state.ts`, `docs/MODEMMANAGER-PROVIDER.md` |
| NETWORKMANAGER ADAPTER — SAVED vs APPLIED, AND NOTHING ELSE | [networkmanager-adapter-saved-vs-applied-and-nothing-else.md](networkmanager-adapter-saved-vs-applied-and-nothing-else.md) | `control/src/providers/network-manager/`, `./providers`, `control/src/backend/nmcli-nm-port.ts`, `/`, `control/test-support/fake-nm/` |
| SMS — LIST / READ AND OBSERVATION, PERMANENTLY | [sms-list-read-and-observation-permanently.md](sms-list-read-and-observation-permanently.md) | `control/src/ports/sms.ts`, `control/src/sms/`, `control/src/sms/readonly-gate.test.ts`, `tests/modem-sms-readonly-gate.test.ts`, `sms/dbus-messaging.ts`, `/` |
| FCC AUTO-UNLOCK — A CATALOG, A POLICY FILE, AND NOTHING ELSE | [fcc-auto-unlock-a-catalog-a-policy-file-and-nothing-else.md](fcc-auto-unlock-a-catalog-a-policy-file-and-nothing-else.md) | `control/src/fcc/`, `docs/FCC-UNLOCK-COVERAGE.md`, `data/dispatcher-fcc-unlock/meson.build`, `/usr/share/ModemManager/fcc-unlock.available.d`, `/etc/ModemManager/fcc-unlock.d`, `${libdir}/ModemManager/fcc-unlock.d` |
| BAND LOCK — THE ONE MODULE THAT IS STRICTER THAN THE FRAMEWORK FLOOR | [band-lock-the-one-module-that-is-stricter-than-the-framew.md](band-lock-the-one-module-that-is-stricter-than-the-framew.md) | `control/src/band/`, `usb-mode/certified-catalog.json` |
| RADIO CAPABILITY TRUTH — THE MODEM'S OWN CATALOG, UNEDITED | [radio-capability-truth-the-modem-s-own-catalog-unedited.md](radio-capability-truth-the-modem-s-own-catalog-unedited.md) | `control/src/radio/`, `src/band/`, `band/certification.ts`, `control/src/radio/{mode-combinations,mode-truth,band-truth}.test.ts`, `control/src/providers/modem-manager/capability-truth.test.ts` |
| SIGNAL AND SIM NORMALIZATION — EVIDENCE, NOT INFERENCE | [signal-and-sim-normalization-evidence-not-inference.md](signal-and-sim-normalization-evidence-not-inference.md) | `hardware/router-parsers.ts`, `/`, `backend/cell-info.ts`, `control/src/observations/sim-evidence.test.ts`, `control/src/observations/normalization.test.ts`, `control/src/providers/modem-manager/signal-richness.test.ts` |
| GPS / LOCATION — A LIVE FIX, AND DELIBERATELY NO HISTORY | [gps-location-a-live-fix-and-deliberately-no-history.md](gps-location-a-live-fix-and-deliberately-no-history.md) | `control/src/ports/location.ts`, `control/src/location/`, `control/src/backend/mm-location.ts`, `ports/location-fence.test.ts`, `location/fix-state.ts`, `location/nmea.ts` |
| USSD — A SESSION PROTOCOL, MODELLED AS ONE | [ussd-a-session-protocol-modelled-as-one.md](ussd-a-session-protocol-modelled-as-one.md) | `control/src/ussd/`, `../redact.ts` |
| eSIM — `blocked` on hardware, not deferred by choice (2026-08-18) | [esim-blocked-on-hardware-not-deferred-by-choice-2026-08-1.md](esim-blocked-on-hardware-not-deferred-by-choice-2026-08-1.md) | `docs/ESIM-DECISION.md`, `src/`, `driver/`, `utils/`, `@ceralive/modem-control`, `packaging/` |
| THE SIM'S OWN NUMBER — READ, REDACTED, NEVER A KEY | [the-sim-s-own-number-read-redacted-never-a-key.md](the-sim-s-own-number-read-redacted-never-a-key.md) | `helpers/logger.ts`, `control/src/backend/mapping.test.ts`, `control/src/redact.test.ts` |
| POLICY | [policy.md](policy.md) | `packaging/`, `docs/FM350-DECISION.md` |
| WORKSPACE / TOOLCHAIN | [workspace-toolchain.md](workspace-toolchain.md) | `irlserver/modem-metrics`, `docs/adr/ADR-STAY-TYPESCRIPT.md`, `control/`, `cli/`, `control/tsconfig.json`, `@typescript/typescript6` |
| CI / CD | [ci-cd.md](ci-cd.md) | `.github/workflows/ci-bun.yml`, `control/**`, `cli/**`, `control/scripts/verify-consumers.ts`, `.github/workflows/ci-packaging.yml`, `packaging/**` |
| DOCS DISCIPLINE (Rule A) | [docs-discipline-rule-a.md](docs-discipline-rule-a.md) | `docs/` |
| Huawei HiLink provider (Todo 24) | [huawei-hilink-provider-todo-24.md](huawei-hilink-provider-todo-24.md) | `control/src/providers/huawei-hilink/`, `hardware/router-parsers`, `hardware/hilink-protocol.ts` |
| ZTE GOFORM PROVIDER (TODO 25) | [zte-goform-provider-todo-25.md](zte-goform-provider-todo-25.md) | `control/src/providers/zte-goform/`, `control/scripts/mf79u-diagnose.sh`, `docs/MF79U-DIAGNOSIS.md` |
| UFI / HIMI PROVIDER (TODO 26) — READ-ONLY, PLUS THE QUALCOMM PROHIBITION FENCES | [ufi-himi-provider-todo-26-read-only-plus-the-qualcomm-pro.md](ufi-himi-provider-todo-26-read-only-plus-the-qualcomm-pro.md) | `control/src/providers/ufi-himi/`, `hardware/router-parsers.ts`, `observations/sources/ufi.ts`, `docs/UFI-DIAG-PROBE.md`, `control/scripts/ufi-himi-capture.sh`, `control/scripts/ufi-himi-evidence.ts` |
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!-- Moved verbatim from AGENTS.md on 2026-10-05 by lean-rules-docs-landing-latam -->

## BAND LOCK — THE ONE MODULE THAT IS STRICTER THAN THE FRAMEWORK FLOOR

`control/src/band/` is the first of the seven capability modules with real verbs
behind it. Two halves:

- **`band-names.ts`** — `MMModemBand` ↔ name, both directions. The D-Bus surface
speaks numbers (`SupportedBands` / `CurrentBands` / `SetCurrentBands` are all
`au`); every operator-facing surface speaks the name (`eutran-3`, `any`). ONE
mapping so the two cannot disagree. It reproduces `mm-enums.h`'s actual shape:
the GSM/UTRAN head (1..20) is IRREGULAR — `UTRAN_2` is 12 while `UTRAN_6` is 8 —
so it is an explicit table and can only ever be one, while every later block is
arithmetic by MM's own construction (`EUTRAN_n = 30 + n`, `CDMA_BCn = 128 + n`,
`NGRAN_n = 300 + n`). Deriving those rather than transcribing ~350 constants is
the point: a transcription is where a wrong band number hides, and a wrong band
number locks a radio to a band the network does not operate on. A value this
build does not name round-trips as `band-<n>` — never dropped, never guessed.
- **`certification.ts` + `certified-bands.json`** — the per-SKU proof gate.

**`encodeBandList` FAILS CLOSED AS A WHOLE.** One unplaceable name rejects the
entire selection rather than silently narrowing it: a partial band set is a
DIFFERENT lock from the one that was asked for, and applying it strands the radio
on bands nobody chose. `decodeBandList` is the mirror — a malformed member is
dropped rather than coerced, and MM's `unknown` (0) is dropped because it means
"the modem did not say", which is not a band an operator can select.

**There is no reset VERB, and there must not be one.** ModemManager releases a
band lock by setting exactly `MM_MODEM_BAND_ANY` (256); `setCurrentBands(modem,
['any'])` IS the reset. Adding a `resetBands` sibling would be a second way to
express one D-Bus call, and the two would eventually disagree about what "no
lock" means.

**Band-lock deliberately requires `certified`, not `capable`, to be OFFERED.**
That is a documented DEVIATION from `support-claim.ts`'s framework floor, and the
framework is right in general: hiding an uncertified-but-working control puts
hardware behind a paperwork gate. For this module the paperwork IS the safety
argument — a band the SIM's network does not operate on registers nowhere, and a
modem that does not honour a reset leaves an operator with no way back short of a
replug they may not be able to reach. So `isBandControlCertified` gates the
control, and the catalog demands FOUR separate proofs (`supportedRead`, `set`,
`readback`, `reset`), each `z.literal(true)`, so a half-certified entry cannot be
expressed at all: a reviewer with three of four has an uncertified SKU and the
file says so by OMITTING it. `readback` exists because an accepted-but-ignored
write is indistinguishable from success at the call site.

**The shipped catalog is EMPTY, and a test pins that.** No fleet modem has been
through the drill: the bench Quectel RM530N-GL's SIM never registers (phase-C
todo 2, blocker B2), so "re-registration proven" cannot be claimed on this bench
today. An entry is added by a human-reviewed commit carrying the transcript,
exactly like `usb-mode/certified-catalog.json`.

`setCurrentBands` runs QUIESCED through the shared `ModemActor` for the same
reason `setRadioModes` does: it re-registers the radio, so NM must stand down
before the bearer drops underneath it rather than after.

Loading
Loading