couch.

Packaging and signing

Couch integration packages are signed, ARMv7 Alpine APKs with one manifest and one executable. The tooling and commands described here are a developer preview. Do not assume they exist on a released device until its release notes say so.

Package layout

An APK contains exactly:

/usr/lib/couch/integrations/ID/
  manifest.json
  bin/couch-plugin-ID

The ID in the directory, manifest, package name, and binary name must match. The manifest's executable path is relative to the integration directory. Every installed payload change, including a rebuilt executable or changed manifest, must bump manifest.version and the matching APK pkgver. Couch stores immutable ID/VERSION slots and rejects different bytes under an existing version. Publishing only an APK revision change such as -r0 to -r1 cannot replace that slot or serve as an integration update. The helper emits -r0 for each new manifest version.

Candidates are unpacked into a private staging root and audited before activation. Couch rejects links, devices, FIFOs, sockets, set-id or world-writable files, files outside the payload, and packages over the file or byte limits. Package scripts and triggers are not allowed.

Build the ARM binary

(cd clients && cargo build --release \
  --target armv7-unknown-linux-musleabihf \
  -p couch-YOUR_ID --bin couch-plugin-YOUR_ID)

Run Cargo from clients/ so it loads that workspace's ARM linker configuration.

Create a signed APK

Run Couch's packaging helper in Alpine with abuild and abuild-sign installed. The ARM binary can be cross-compiled on Linux or macOS, but the APK helper itself needs an Alpine packaging shell or container. Create a dedicated development key outside the source checkout:

KEY_DIR="$HOME/.local/share/couch-integration-signing"
umask 077
mkdir -p "$KEY_DIR"
openssl genrsa -out "$KEY_DIR/developer.rsa" 4096
openssl rsa -in "$KEY_DIR/developer.rsa" -pubout \
  -out "$KEY_DIR/developer.rsa.pub"
chmod 600 "$KEY_DIR/developer.rsa"

# Inside the disposable Alpine packaging container:
install -Dm644 "$KEY_DIR/developer.rsa.pub" \
  /etc/apk/keys/developer.rsa.pub

abuild -r creates and reads an intermediate APK index, so that disposable build container must trust the matching public key. Its /etc/apk/keys is build-only. Keep the private key on the packaging host or mount it only into that container for packaging. Never copy it to a device. Device trust remains separate in Couch's repository-scoped directories or an explicitly selected CLI key directory; do not put an integration key in the device's global Alpine /etc/apk/keys.

tools/integrations/build-apk.sh \
  YOUR_ID 0.1.0 \
  clients/target/armv7-unknown-linux-musleabihf/release/couch-plugin-YOUR_ID \
  clients/couch-YOUR_ID/plugin.json \
  "$KEY_DIR/developer.rsa" \
  build/integrations

The helper builds a package named couch-integration-YOUR_ID, sets its architecture to armv7, and adds no install scripts.

Trust

Both sideload and repository installation require a valid native APK signature. Sideload does not mean unsigned. The integration-capable runtime embeds the official public key in couch-confd, which arrives through the signed core update. Official web or CLI installation needs no separate key-file transfer. The CLI's historical default /opt/couch/integration-keys/official is now a selector for the embedded key, materialized in the private package store at /opt/couch/integrations/.official-keys (COUCH_INTEGRATIONS_DIR overrides the store root). Files added to the historical directory do not add official trust.

For a custom feed, use the paired web Integrations page to enter its HTTPS base URL and public PEM key, compare the displayed SHA-256 fingerprint with the owner's published value, then select Trust repository. The URL and key are persisted only after confirmation, with a trust directory scoped to that repository and fingerprint. Couch does not automatically fetch or trust a custom feed's key. Removing its repository record leaves installed packages and saved connection settings intact.

For a manual custom or developer CLI install, provision only the public key in a dedicated path such as /opt/couch/integration-keys/custom/my-feed and pass --keys-dir explicitly. That option still reads the selected custom directory; it does not inherit official or unrelated system keys. Never commit a private signing key or copy it to a device or package.

For a repository, collect signed APKs and create a signed Alpine index:

tools/integrations/build-repository.sh \
  "$KEY_DIR/developer.rsa" \
  build/integrations \
  build/repository

Host that directory over HTTPS and register its public key through the paired web page, or select a manually provisioned custom key directory in the CLI.

Manage packages in the paired web UI

After the first integration-capable core update, open Integrations in the paired configuration web UI. Refresh packages verifies signed indexes from the built-in Stable and Preview sources and any confirmed custom repositories. Choose Install, Update to …, Restore previous version, or Remove package as appropriate. Operations report progress and failure; removal retains saved connections and credentials. The page distinguishes a verified previous-version fallback from a missing or invalid package. Downloads and installation follow explicit user actions; package updates are not automatic.

An older retained core does not force a second upgrade before integrations can be enabled. The configuration file contains an old-readable projection and a complete modern extension. Rollback keeps built-in devices usable; external controls require the capable core. If the old core saves edits, re-upgrade keeps those edits and offers an explicitly restorable recovery export rather than automatically reintroducing deleted bindings. The recovery import replaces the whole configuration, so export the current house first.

The following SSH CLI remains supported for development, signed sideloads and manual repair. Web repository registration and CLI --repository selection are separate workflows.

Install and operate on a remote

The normal HA100 SSH server runs inside Alpine, so an SSH session sees /opt/couch and /opt/couch/integration-keys directly. The USB serial and early recovery shells run in the outer initramfs, where the same files are under /mnt/alpine. Detect which shell you have before copying files or adding chroot:

if [ -f /etc/alpine-release ]; then
  echo "Alpine shell"
elif [ -f /mnt/alpine/etc/alpine-release ]; then
  echo "outer initramfs shell"
fi

These commands require an integration-capable Couch runtime. The current .170 device release predates this host and cannot install integration APKs. After an eligible runtime is installed, this probe exits successfully:

/opt/couch/runtime/current/couch-confd \
  --supports-integration-protocol=1

The development CLI is exposed under couch-confd integrations. In the normal Alpine SSH session, name the exact trust directory for that package source:

/opt/couch/runtime/current/couch-confd integrations \
  --keys-dir /opt/couch/integration-keys/custom/developer \
  install-sideload /tmp/couch-integration-YOUR_ID-0.1.0-r0.apk
/opt/couch/runtime/current/couch-confd integrations \
  --keys-dir /opt/couch/integration-keys/custom/my-feed \
  install-repository couch-integration-YOUR_ID \
  --repository https://packages.example.invalid/couch
/opt/couch/runtime/current/couch-confd integrations list
/opt/couch/runtime/current/couch-confd integrations rollback YOUR_ID
/opt/couch/runtime/current/couch-confd integrations remove YOUR_ID

For example, copy a development package and its public key from the build host into the normal Alpine SSH environment, then admit it through Couch:

ssh root@couch.local \
  'test -f /etc/alpine-release && \
   mkdir -p /opt/couch/integration-keys/custom/developer'
scp "$KEY_DIR/developer.rsa.pub" \
  root@couch.local:/opt/couch/integration-keys/custom/developer/
scp build/integrations/couch-integration-YOUR_ID-0.1.0-r0.apk \
  root@couch.local:/tmp/
ssh root@couch.local \
  '/opt/couch/runtime/current/couch-confd integrations \
    --keys-dir /opt/couch/integration-keys/custom/developer \
    install-sideload /tmp/couch-integration-YOUR_ID-0.1.0-r0.apk'

From an outer initramfs or serial shell, prefix the runtime command with chroot /mnt/alpine and use /mnt/alpine/opt/... or /mnt/alpine/tmp/... when manipulating files outside the chroot. Command arguments after chroot remain Alpine paths such as /opt/couch/integration-keys/custom/developer and /tmp/package.apk.

Installing with apk add directly is not equivalent: it bypasses Couch's payload audit, protocol handshake, immutable slot store, activation record, and rollback path.

Custom repositories can be saved in the paired web UI as described above. A manual CLI installation still supplies its URL and dedicated key directory on each invocation:

/opt/couch/runtime/current/couch-confd integrations \
  --keys-dir /opt/couch/integration-keys/custom/acme-lab \
  install-repository couch-integration-YOUR_ID \
  --repository https://packages.example.invalid/couch

Repository installation verifies the signed index, then verifies the package again during admission. The store keeps immutable version slots and active and previous slot hashes paired in one atomic state record. A candidate must pass payload audit and protocol handshake before activation. Rollback swaps the two hashes in that record.

Removing a package does not erase a user's connection record. Commands stop until a compatible package is installed again.

An admitted integration is trusted native code. The host starts it in a separate process and drops root to the unprivileged integration identity; on the HA100 it grants only the supplemental network group needed to open normal Internet sockets. Protocol framing, deadlines, capability checks, and process retirement contain failures. This is privilege separation, not a complete sandbox: integrations share an unprivileged UID, can reach the LAN, and do not run in separate mount or network namespaces. Install code only from a feed whose signing key you trust.

Distribution checklist

Hosting a feed on GitHub

An APK feed is static files: a signed APKINDEX.tar.gz and its signed APKs under an architecture directory. The public dangerouslaser/couch-integrations repository holds publication policy, the immutable source graph, and the Pages build and deployment workflow. The staged schema-2 source-pin graph pins the shared Couch tooling and every selected integration repository independently at full commits. Each integration repository owns integration.json, plugin.json, its lock file, implementation, and reusable-harness tests. Couch remains the single owner of the SDK, framed protocol, admission harness, and APK tooling; integration repositories pin couch-plugin and couch-sdk to the same full Couch commit and reuse couch-plugin::testing instead of copying the protocol. GitHub Pages serves this layout:

preview/armv7/APKINDEX.tar.gz
preview/armv7/couch-integration-YOUR_ID-0.1.0-r0.apk
stable/armv7/APKINDEX.tar.gz

The feed base URLs are:

https://dangerouslaser.github.io/couch-integrations/preview
https://dangerouslaser.github.io/couch-integrations/stable

The installer adds armv7 when it fetches the index. preview initially contains the Denon integration. stable serves a valid signed empty index and contains no packages until an integration has production-tier hardware evidence; preview hosting does not make Denon stable.

The official public key is couch-integrations.rsa.pub. Its PEM file SHA-256 is:

80f3a73d86759cda103cb4f9a876cd4caee9d25c235c6d782b4be8a900b2696c

The integration-capable runtime already embeds that public key. Inspect the published fingerprint through a trusted source when auditing it; do not add it to Alpine's global key store. The complete manual preview install command is:

/opt/couch/runtime/current/couch-confd integrations \
  install-repository couch-integration-denon \
  --repository https://dangerouslaser.github.io/couch-integrations/preview

The production .170 runtime still predates the package host; publishing the feed does not make that command available until an integration-capable runtime is installed. GitHub Packages does not offer a native APK registry among its supported formats. Pages is suitable for an initial public feed within its limits: 1 GB published size and a soft 100 GB monthly bandwidth limit. Larger feeds can keep source and CI on GitHub and move static delivery to object storage.

Publishing needs a trusted post-merge or approved release workflow, protected signing-key access, and Pages deployment permissions. Never expose the signing key to pull-request builds. Require admission before publication, publish only eligible catalog tiers, retain prior immutable package versions for rollback, record the integration repository and commit, SDK repository and commit, tooling repository and commit, and binary and manifest hashes in schema-2 provenance. Deploy the complete index and package set together. Archives and release receipts can also live in GitHub Releases.

The core update supplies the official trust key and the paired web page retains confirmed custom repositories. The CLI does not persist its --repository argument, so manual installations still name the preview or stable URL. Integration releases can then ship independently of the core runtime. Official key rotation requires a reviewed core trust update and coordinated feed signing; it cannot be authorized by files added to the old default key directory. Changing hosting does not remove the first runtime update needed to install the plugin host and package manager.

Source references