Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
967b25b
feat(packaging): provenance-verified upstream pin manifest
andrescera Jul 15, 2026
1e0e936
feat(control): domain model with 4-part identity and orthogonal state
andrescera Jul 15, 2026
a5ac47b
feat(control): D-Bus transport with independent-producer conformance
andrescera Jul 15, 2026
69ba38e
feat(packaging): bookworm MM 1.24 stack rebuilds (zero source patches)
andrescera Jul 15, 2026
dca7747
feat(control): port contracts with enforced ownership matrix + desire…
andrescera Jul 15, 2026
7512ddc
test(control): MM-faithful fake D-Bus service + stateful fake NM harness
andrescera Jul 15, 2026
6ba10f6
test(packaging): contract suite with daemon smoke + artifact releases
andrescera Jul 15, 2026
8093646
feat(control): epoch-scoped lifecycle-correct MM observer
andrescera Jul 15, 2026
5a492d4
feat(control): MM feature detection + stable identity ladder
andrescera Jul 15, 2026
7c315d7
feat(control): D-Bus mutations, Signal.Setup lifecycle, shared disrup…
andrescera Jul 15, 2026
9c4594f
feat(control): evidence-gated recovery ladder + power capability cont…
andrescera Jul 15, 2026
eeb60be
feat(control): per-modem data-usage sampler over cumulative counters
andrescera Jul 15, 2026
da9e24e
feat(control): device classes + certified USB-mode transition transac…
andrescera Jul 15, 2026
cae14b9
feat(control): device-exact NmcliNmPort with atomic Auto-APN transitions
andrescera Jul 15, 2026
7921c9d
feat(cli): bench CLI with compiled cross-arch D-Bus probe smoke
andrescera Jul 15, 2026
842b27b
feat(cli): per-SKU certification capture with transition evidence
andrescera Jul 15, 2026
1886a2f
docs+release: bench runbooks, @ceralive/modem-control 0.1.0, packagin…
andrescera Jul 15, 2026
7811d4d
fix(control): bound D-Bus connect attempts so reconnect can't stall
andrescera Jul 15, 2026
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
75 changes: 40 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,51 +128,56 @@ jobs:
run: npm publish --access public

build-deb:
name: Build .deb set (specified version injection)
name: Build .deb set + contract + daemon smoke (real artifacts)
needs: [tag-guard, test]
# Runs on the runner host (NOT inside a container): build-bookworm.sh and the contract /
# smoke scripts each launch their OWN debian:bookworm containers, so the job needs the
# host docker daemon, not to be a container itself.
runs-on: ubuntu-latest
container: debian:bookworm
steps:
# git for actions/checkout; devscripts/dpkg-dev provide dch + dpkg-parsechangelog.
- name: Install build prerequisites
run: |
apt-get update
apt-get install -y --no-install-recommends \
git ca-certificates devscripts dpkg-dev ccache

- uses: actions/checkout@v7

# Strip the leading `v` and inject <upstream>-<rev>~ceraliveX.Y.Z as the top
# debian/changelog entry of each of the 4 sources via
# dch --force-bad-version --newversion "<version>" "CeraLive rebuild"
# (--force-bad-version is REQUIRED — the tilde version is LOWER than the pinned
# <upstream>-1 changelog top; plain `dch --newversion` refuses it, per dch(1)).
# Until the packaging wave lands real changelogs this documents the exact per-source
# invocation and exits 0.
- name: Inject encoded .deb version into each source changelog
# arm64 .debs are built under full-system QEMU (never cross-compiled); register binfmt.
- name: Set up QEMU (arm64 emulation)
uses: docker/setup-qemu-action@v3

# Real rebuilds. RELEASE_VERSION injects <upstream>-<rev>~ceraliveX.Y.Z into a COPY of
# each source's debian/changelog inside the container (the committed tree is never
# mutated), builds in bootstrap order on native amd64 + QEMU arm64, and asserts the
# 9-package runtime closure per arch. --force-bad-version is applied by inject-deb-version.
- name: Build the MM 1.24 stack (.deb) — amd64 + arm64
env:
DEBFULLNAME: CeraLive CI
DEBEMAIL: ci@ceralive.tv
run: bash packaging/ci/inject-deb-version.sh "${{ github.event.inputs.tag }}"
RELEASE_VERSION: ${{ github.event.inputs.tag }}
run: |
packaging/ci/build-bookworm.sh amd64
packaging/ci/build-bookworm.sh arm64

- name: Assemble release manifest (tag -> deb versions)
# Package contract: amd64 full (metadata / closure install / upgrade / rollback /
# coherence / ordering / tag-guard / piuparts); arm64 metadata-level (apt-install under
# QEMU is prohibitively slow on a runner — the metadata/coherence/ordering proofs run).
- name: Package contract suite
run: |
mkdir -p dist
{
echo "tag: ${{ github.event.inputs.tag }}"
echo "version: ${{ needs.tag-guard.outputs.version }}"
echo "deb_version_suffix: ~ceralive${{ needs.tag-guard.outputs.version }}"
echo "sources: [ModemManager, libmbim, libqmi, libqrtr-glib]"
} > dist/release-manifest.txt
cat dist/release-manifest.txt

# `warn` (not `error`): no real .deb exists until the packaging wave lands recipes;
# the manifest still uploads so the release artifact set is present.
packaging/ci/test-package-contract.sh amd64
packaging/ci/test-package-contract.sh arm64

# Daemon smoke: system D-Bus + polkit + NetworkManager 1.42, start MM, busctl
# introspect the root ObjectManager, mmcli --version == 1.24.0, udev/FCC/GIR/Vala paths.
- name: Daemon smoke (amd64)
run: packaging/ci/daemon-smoke.sh amd64

# Per-release manifest: the release tag -> the 9 runtime deb versions per arch
# (Phase-B apt publication consumes this as its package -> source -> version matrix).
- name: Generate release manifest
run: packaging/ci/generate-release-manifest.sh "${{ github.event.inputs.tag }}"

# Upload the REAL built .debs (both arches: 9 runtime + dev/gir/dbgsym) + the manifest as
# CI release artifacts. Bench devices download this set and `apt install ./*.deb`.
- name: Upload .deb artifacts + release manifest
uses: actions/upload-artifact@v7
with:
name: modem-stack-debs-${{ needs.tag-guard.outputs.version }}
path: |
dist/**
packaging/**/*.deb
if-no-files-found: warn
dist/release-manifest.txt
packaging/build/amd64/*.deb
packaging/build/arm64/*.deb
if-no-files-found: error
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ node_modules/
dist/
*.tsbuildinfo

# Packaging build artifacts (.deb output from packaging/ci/build-bookworm.sh)
packaging/build/

# Local QA evidence (agent-generated, never committed)
test-results/

Expand Down
14 changes: 11 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ bun run typecheck # tsc --noEmit (strict + exactOptionalPropertyTypes)
```

`packaging/` runs in a `debian:bookworm` container; its contract/verification scripts live
under `packaging/ci/`.
under `packaging/ci/`. The four sources' `debian/` recipes are checked in at
`packaging/<Source>/debian/` (`ModemManager`, `libmbim`, `libqmi`, `libqrtr-glib` —
byte-identical to their pinned salsa commits except the bookworm adaptations documented in
`packaging/BOOKWORM-ADAPTATIONS.md`). `packaging/ci/build-bookworm.sh <amd64|arm64>` rebuilds
them from source in the mandatory bootstrap order (`libqrtr-glib → libmbim → libqmi →
modemmanager`) via a temporary local apt repo, on native amd64 or full-system-QEMU arm64
(never cross-built), and asserts the 9-package runtime closure. `.deb` output lands in the
gitignored `packaging/build/<arch>/`.

## CI / CD

Expand All @@ -78,8 +85,9 @@ major action versions, per-manager caches, weekly grouped Dependabot, test-befor
`control/**` and `cli/**`: `bun install` → Biome check → `tsc --noEmit` → `bun test`.
`cancel-in-progress: true`.
- **`.github/workflows/ci-packaging.yml`** — paths-filtered PR + push(`main`) container lane
for `packaging/**`: runs the packaging contract scripts in `debian:bookworm`. At this
stage the contract lane is a stub (real recipes land in a later task).
for `packaging/**`: runs the packaging contract scripts in `debian:bookworm`. The four
`debian/` recipes and `build-bookworm.sh` now exist; the full contract suite (metadata /
closure / upgrade / rollback / daemon smoke) lands in a later task.
`cancel-in-progress: true`.
- **`.github/workflows/release.yml`** — the **single** release workflow, owns **both**
artifacts. `workflow_dispatch` with a `tag` input. Job graph:
Expand Down
45 changes: 45 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# modem-control — bench CLI

The iteration surface for the CeraLive modem stack. It drives the
[`@ceralive/modem-control`](../control/) library against real modems on a bench device
to mature the package and (with `certify`) capture per-SKU certification bundles.

## Commands

| Command | What it does |
|---------|--------------|
| `probe` | One-shot stack snapshot: identities (+ identity-ladder resolution), lifecycle state, MM feature detection, read-only enrichment, cell info, and classified USB devices. Ends with `PROBE OK: external-auth, objects=<n>`. |
| `watch` | Live event stream. Prints `+ ADDED` / `~ CHANGED` / `- REMOVED` per change and `! SOURCE-UNAVAILABLE` on a bus drop. A bus drop / MM restart marks a modem source-unavailable with its row **retained** — never a removal (A3.1 epoch authority). `--duration <ms>` / `--events <n>` bound the run; otherwise it runs until Ctrl-C. |
| `apply --policy <file>` | Reads a JSON/YAML desired-state policy, derives the modem's durable binding key (refusing an ambiguous identity), runs the reconcile planner, applies the ops, and prints one receipt per policy dimension. |
| `set-usb-mode <slot> <target> --confirm` | Runs a certified USB-mode transition. **Omitting `--confirm` refuses the transition with zero side effects.** `<target>` is one of `qmi` / `mbim` / `ecm-ncm`. |
| `certify <slot>` | Captures a redacted, schema-validated certification bundle: `lsusb -v`, `usb-devices`, the slot's udev properties, an `mmcli -K` dump, a redacted `GetManagedObjects`, and a bounded signal window. `--transition <mode>` adds transition evidence (before/after descriptors, the executed AT command, and the port-drop / re-enumeration timeline) shaped to drop straight into an A4.2 catalog entry. Prints `CERTIFY OK: sha256=<hash> …` — the sha256 is the value a reviewer records in a catalog entry's `evidenceBundleSha256`. Real captures are marked `synthetic: false`; ICCID / IMSI / EID are masked; a malformed capture exits non-zero with a clear error rather than writing a broken bundle. `--output <file>` writes the bundle JSON (default stdout). |
| `usage` | Prints the data-usage sampler snapshot (per-slot cumulative-cycle bytes; advisory threshold). |
| `unlock-pin [slot]` / `unlock-puk [slot]` | Prompts for the PIN / PUK with **terminal echo disabled** (the secret is never printed back) and submits it. |

### Global options

- `--bus-address <addr>` (or env `MODEM_CONTROL_BUS_ADDRESS`) — the D-Bus bus address.
Defaults to the system bus (`unix:path=/var/run/dbus/system_bus_socket`), where a real
ModemManager lives. This is the injection point that points the CLI at a fake MM service
for harness-driven tests and the compiled-probe smoke.
- `--destination <name>` — override the ModemManager bus name.

### Policy file

`apply --policy` accepts JSON or YAML (by file extension). The file carries the desired
intent only — `apply` binds it to the selected modem's live identity:

```json
{
"slot": "Modem/0",
"enabled": true,
"connection": { "apn": "auto", "ipFamily": "ipv4v6" },
"roaming": false,
"radio": { "preferenceOrdered": ["5gnr", "lte", "umts", "gsm"] },
"recovery": { "enabled": false },
"usage": { "cycleDay": 1, "thresholdBytes": 5000000000 }
}
```

## Develop

From the repository root (single Bun workspace):

```sh
bun install
bun run typecheck # tsc --noEmit (strict)
bun run lint # Biome
dbus-run-session -- bun test cli # harness-driven CLI integration tests
```

The integration tests run the dev/TS CLI against the fake ModemManager + NetworkManager
(A2.3). They need a session bus, hence `dbus-run-session`.

## Compiled binaries + cross-arch probe smoke

The CLI is compiled to standalone binaries for `arm64` + `amd64`, and a self-contained
smoke harness proves a real D-Bus probe on both architectures:

```sh
cli/smoke/build-binaries.sh # -> cli/dist/modem-control-{amd64,arm64} + smoke harness
cli/smoke/run.sh # amd64 native + arm64 (Docker QEMU) + a negative sanity run
```

`run.sh` proves, on both arches, that the compiled binary completes an EXTERNAL-auth
handshake, `GetManagedObjects` returns real data, a signal is received, and a bus drop is
discriminated from a real removal. The negative run spins a modem-less fake and MUST fail,
proving the smoke exercises the real dependency rather than passing unconditionally. The
arm64 run reuses the `docker run --platform linux/arm64` QEMU path.

## Hardware-gated: on-device system-bus probe `[PARTIAL]`

Run against a **real** ModemManager on a bench device's system bus. This path is gated on
hardware and is not exercised in CI.

```sh
# On the bench device, with the packaged ModemManager stack installed and running:
./modem-control probe
```

Expected: the run ends with a line of the form

```
PROBE OK: external-auth, objects=<n>
```

where `<n>` is the number of managed objects ModemManager reports (modems + SIMs +
bearers). Capture the full output to the evidence path `A6.1/hil-system-bus.txt`.

If the device's ModemManager is on a non-default address, point the CLI at it with
`--bus-address` or `MODEM_CONTROL_BUS_ADDRESS`.
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"modem-control": "./src/index.ts"
},
"dependencies": {
"@ceralive/modem-control": "workspace:*"
"@ceralive/modem-control": "workspace:*",
"zod": "4.4.3"
},
"scripts": {
"test": "bun test",
Expand Down
24 changes: 24 additions & 0 deletions cli/smoke/build-binaries.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Cross-compile the bench CLI and the smoke harness for amd64 + arm64.
#
# Bun 1.3.14 cross-compiles with `--compile --target=bun-linux-<x64|arm64>` (verified:
# the target flag downloads the matching bun runtime and emits a standalone ELF for that
# architecture). Outputs land in cli/dist/ (gitignored).
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DIST="${ROOT}/cli/dist"
mkdir -p "${DIST}"
cd "${ROOT}"

for pair in "x64:amd64" "arm64:arm64"; do
bun_target="bun-linux-${pair%%:*}"
arch="${pair##*:}"
echo "== compiling modem-control (${arch}) =="
bun build --compile --target="${bun_target}" --outfile="${DIST}/modem-control-${arch}" cli/src/index.ts
echo "== compiling modem-control-smoke (${arch}) =="
bun build --compile --target="${bun_target}" --outfile="${DIST}/modem-control-smoke-${arch}" cli/smoke/harness.ts
done

echo "== built binaries =="
ls -1 "${DIST}"
Loading