Skip to content
Onboard Software Deployment via Inventory Service

Onboard Software Deployment via Inventory Service

Andi Lamprecht Andi Lamprecht ·· 16 min read· Draft

PRD: Onboard Software Deployment via Inventory Service

FieldValue
StatusDraft
OwnerTBD
ContributorsTBD
Date2026-04-21

1. Executive Summary

Problem Statement: Deploying the MAVLink Shim onboard software to a UAV requires manually handling mTLS certificates, configuring endpoint URLs, copying binaries, and restarting services — all as separate steps with no unified workflow. The same problem exists for virtual UAV simulators: the Simulator Controller today uses a custom cloud-init startup script that installs AOS bundles independently from any cert or registration logic, leaving simulators outside the inventory lifecycle.

Proposed Solution: Each Inventory Service deployment (one per environment — dev, staging, prod, client-X) exposes a /install endpoint that serves a ready-to-run bash install script (modeled after get-docker.sh). The script is already scoped to its environment because it is served by that environment’s Inventory Service instance — no env parameter needed. For physical UAVs, an operator copies the URL from the UI, SSHs into the UAV, and runs it interactively. For simulators, the Simulator Controller embeds the same script call in the VM’s cloud-init metadata, passing the UAV ID non-interactively.

Success Criteria:

  1. An operator can fully install and start the MAVLink Shim on a physical UAV by running a single curl ... | bash command obtained from the Inventory Service UI of the target environment.
  2. The install URL contains no UAV ID and no env parameter — the Inventory Service instance itself determines the scope.
  3. The script handles the full registration lifecycle: submit registration request → wait for approval → fetch issued cert bundle → install and start.
  4. Deploying a new Inventory Service instance for a new client environment automatically makes its /install endpoint available with no code change.
  5. The installed mavlink_shim successfully connects to Avatar via mTLS within 60 s of the script completing after approval.
  6. A new simulator VM spawned by the Simulator Controller is fully registered, approved, and running mavlink_shim with correct certs automatically — no manual steps required after kubectl apply.

Scope Clarity: A — requirements are clear and bounded for MVP.


2. User Experience & Functionality

User Personas

PersonaDescription
Field TechnicianSSHes into a UAV on the ground, runs the generated script interactively, confirms the shim is running. No knowledge of certs or environment config required.
Client OperatorReceives the install URL from DroneUp for their dedicated Inventory Service instance. Runs it on their UAV fleet. May not know what an mTLS cert is.
Simulator Controller (automated)The uncrew-simulator-controller Kubernetes operator that spawns GCP VMs. Passes the UAV ID and the target env’s Inventory Service install URL via cloud-init metadata — no human in the loop.

User Stories & Acceptance Criteria

US-01: Get install command from Inventory Service UI

As a platform engineer, I want the Inventory Service UI to show a ready-to-run install command so that I can hand it to a field technician before the UAV is even registered.

  • The Inventory Service UI shows an “Install Onboard Software” section with a one-liner:
    curl -fsSL https://<this-inventory-instance>/install | bash
  • The URL contains no UAV ID and no env parameter — it is served by the specific Inventory Service instance the operator is viewing.
  • The same URL works for any UAV being enrolled into this environment.
  • A “Copy” button copies the command to clipboard.

US-02: Script collects UAV ID at runtime

As a field technician, I want the script to ask me for the UAV ID (or read it from hardware) so that I do not need to know it before running the command.

  • On startup the script attempts to read the hardware serial number from /sys/firmware/devicetree/base/serial-number (Skynode).
  • If auto-detection succeeds, the script prints the detected ID and asks for confirmation: Detected UAV ID: <id>. Continue? [Y/n]
  • If auto-detection fails or the operator declines, the script prompts: Enter UAV ID:
  • The resolved UAV ID is used for all subsequent Inventory Service calls.

US-03: Script registers UAV and waits for approval

As a field technician, I want the script to handle registration automatically so that I do not need to use a separate device to trigger cert issuance.

  • The script calls RegisterNewUav with the resolved UAV ID.
  • If the UAV is already APPROVED, the script skips registration and proceeds to cert fetch.
  • If the UAV is REQUESTED, the script prints:
    Registration submitted. Waiting for approval in Inventory Service...
    (Press Ctrl+C to cancel and resume later)
  • The script polls every 10 s for up to 30 min. On approval it proceeds automatically.
  • If the UAV is REJECTED, the script exits with a clear error and exit code 1.

US-04: Install script downloads binary and factory certificates

As a field technician, I want the install script to download everything the MAVLink Shim needs so that I do not have to manually copy files to the UAV.

  • The script downloads the correct onboard software(or AOS bundle) for the UAV’s CPU architecture (aarch64 for Skynode, x86_64 for simulator VMs) from Google Artifact Registry.
  • After approval, the script fetches the issued cert bundle from the Inventory Service:
    • CA cert for this environment’s certificate authority
    • UAV client certificate
    • UAV client private key
  • Certs are written to /etc/mavlink-shim/certs/ with 0600 permissions.
  • The script writes /etc/mavlink-shim/config.env with all required environment variables.

US-05: Environment configuration baked into the script

As a field technician, I want the installed software to automatically connect to the correct Avatar endpoint so that I do not configure any URLs manually.

  • The install script is generated by the Inventory Service at request time and contains values from that instance’s own deployment config: Avatar gRPC endpoint, cert domain template.
  • The config.env written to the UAV contains at minimum:
    MAVLINK_SHIM_AVATAR_URL=<from inventory deployment config>
    MAVLINK_SHIM_ROOT_CERT_PATH=/etc/mavlink-shim/certs/ca.crt
    MAVLINK_SHIM_CLIENT_CERT_PATH=/etc/mavlink-shim/certs/client.crt
    MAVLINK_SHIM_CLIENT_KEY_PATH=/etc/mavlink-shim/certs/client.key
  • No hardcoded endpoint URLs exist in shared code — each Inventory Service instance serves its own values.

US-06: Script installs and starts the service

As a field technician, I want the script to install and start mavlink_shim as a system service so that it runs automatically on UAV reboot.

  • The script installs docker images and run them.
  • The script installs a systemd unit file and runs systemctl enable --now mavlink-shim.
  • If mavlink_shim is already running (upgrade case), the script stops it, replaces the binary and certs, then restarts.
  • The script prints a final status line; exits non-zero on any failure.

US-07: Client environment isolation via separate deployments

As a platform engineer, I want each client to have a fully isolated Inventory Service deployment with its own CA so that a UAV in one client’s environment cannot authenticate to another client’s Avatar instance.

  • Each Inventory Service deployment has its own CA keypair configured at deploy time (Helm values / Google Secret Manager) — no CA is shared between deployments.
  • The install script served by client-acme’s Inventory Service fetches certs signed by the client-acme CA only.
  • A new client environment is onboarded by deploying a new Inventory Service instance with a new CA — no application code change required.

US-08: Idempotent re-run (upgrade path)

As a platform engineer, I want to re-run the install command to upgrade the MAVLink Shim so that upgrades use the same workflow as first installs.

  • Re-running the script replaces the binary and refreshes the cert bundle, then restarts the service.
  • The script does not fail if the service is not currently running.
  • The previous binary is backed up before replacement.

US-09: Headless (non-interactive) mode for simulators

As the Simulator Controller, I want to invoke the install script non-interactively with a pre-known UAV ID so that simulator VMs are fully provisioned without human input during cloud-init startup.

  • The script accepts UAV_ID as an environment variable: UAV_ID=sim-abc123 curl -fsSL .../install | bash
  • When UAV_ID is set, the script skips hardware detection and all interactive prompts.
  • When no TTY is present and UAV_ID is unset, the script exits with a clear error rather than hanging.
  • The Simulator Controller sets UAV_ID equal to the GCP VM instance name (deterministic, unique per simulator).

US-10: Auto-approval for simulators

As a platform engineer, I want simulator UAVs to be approved automatically on registration in sandbox/dev so that spinning up a new simulator requires no manual approval step.

  • The Inventory Service deployment config includes an AUTO_APPROVE flag (default false).
  • When AUTO_APPROVE=true, RegisterNewUav transitions the UAV directly to APPROVED and issues certs immediately.
  • AUTO_APPROVE is only set for sandbox/dev Inventory Service deployments — never for staging, prod, or client instances.
  • The UI shows a persistent warning banner when running against an auto-approve instance.
  • Auto-approved UAVs are visually distinguished in the UAV list (e.g. “Auto” tag).

Non-Goals (MVP)

Out of ScopeRationale
Fleet-wide batch installMVP targets single UAV; fleet tooling deferred
Web UI showing real-time installation progressScript runs on UAV; UI cannot observe it without telemetry infrastructure
Automatic cert rotation / renewalSeparate Inventory Service operation; deferred
Rollback triggered from the UIScript backs up previous binary; rollback is manual for MVP
Deploying AI Node softwareSeparate hardware target; deferred to v1.1
Client self-service environment creationDroneUp engineers deploy new Inventory Service instances; self-service deferred
Software signature verificationDeferred to v1.1

3. Regulatory & Compliance

The MAVLink Shim runs on an airborne vehicle and directly interfaces with the PX4 autopilot. Changes to onboard software deployment should be reviewed against the DroneUp 44807 exemption scope.
AreaStatusNotes
FAA 44807 ExemptionReview requiredOnboard software changes may require notification to or re-assessment by the FAA exemption holder. Confirm with the safety team before deploying to production UAVs.
DO-178CNot applicable (MVP)MAVLink Shim is not currently certified under DO-178C. If certification is pursued, the install script would need to produce a Configuration Index record.
Software Configuration ManagementIn scopeThe Inventory Service must record: UAV ID, binary version installed, operator identity, and install timestamp. Minimum CM traceability record.
ITARConfirm with legalMAVLink Shim interfaces with autopilot and implements geofencing / failsafe logic. Confirm with legal whether the binary is ITAR-controlled before distributing to client environments.
PII / Secret HandlingIn scopePrivate keys fetched during install must never be logged. The script must delete any temp files containing key material after writing to final paths.
Client Data IsolationIn scopeEach client’s CA keypair lives in that client’s Inventory Service deployment secrets — no cross-tenant access is possible at the infrastructure level.

4. Technical Specifications

Architecture Overview

Each Inventory Service deployment is scoped to one environment. The install script served by any given instance is pre-configured with that instance’s Avatar endpoint and CA — the UAV or VM that runs the script automatically gets the right environment:

─── Physical UAV path ──────────────────────────────────────────────

Inventory Service UI  (e.g. client-acme's instance)
      │  Operator clicks "Get install command"
      ▼
      curl -fsSL https://inventory.client-acme.droneup.cloud/install | bash
                  │
                  │  Operator SSHes into UAV, runs interactively
                  ▼
          Install Script  (Avatar URL + CA baked in from this instance's config)
                  │
                  ├── 1. Detect hardware serial / prompt "Enter UAV ID:"
                  ├── 2. RegisterNewUav(uavID)  →  this Inventory Service
                  ├── 3. Poll until APPROVED  ←  [Admin approves in UI]
                  ├── 4. Fetch cert bundle (token-gated)
                  ├── 5. Download mavlink_shim binary (aarch64)
                  ├── 6. Write /etc/mavlink-shim/certs/ + config.env
                  └── 7. systemctl enable --now mavlink-shim

─── Simulator path ─────────────────────────────────────────────────

kubectl apply -f simulator.yaml
      │
      ▼
Simulator Controller (K8s operator)
      │  Reconciles Simulator CR
      │  Knows target Inventory Service URL from its own config
      ├── Generates VM name  →  used as UAV_ID
      └── Creates GCP VM with cloud-init metadata:
              startup-script: |
                UAV_ID=sim-<name> \
                curl -fsSL https://inventory.dev.droneup.cloud/install | bash
                  │
                  │  VM boots, cloud-init runs startup script
                  ▼
          Install Script  (headless: UAV_ID set, no TTY prompts)
                  │
                  ├── 1. UAV_ID from env var (skip hardware detect)
                  ├── 2. RegisterNewUav(uavID)  →  this Inventory Service
                  ├── 3. AUTO_APPROVE=true → instant APPROVED + certs issued
                  ├── 4. Fetch cert bundle (token-gated)
                  ├── 5. Download mavlink_shim binary (x86_64 for GCP VM)
                  ├── 6. Write /etc/mavlink-shim/certs/ + config.env
                  └── 7. systemctl enable --now mavlink-shim

─── Common ─────────────────────────────────────────────────────────

      mavlink_shim
            │  Reads config.env
            └── mTLS connect → Avatar (endpoint baked into config.env by install script)

Affected Repos

RepoLanguageRole
uncrew-inventory-serviceGo / gRPCServes /install script (values from own config); cert issuance; AUTO_APPROVE deployment flag
uncrew-mavlink-shimC++ / CMakeThe onboard binary; must publish versioned aarch64 and x86_64 artifacts to Google Artifact Registry
uncrew-apollo-frontendReact (assumed)Inventory Service UI — shows install command; auto-approve warning banner
uncrew-simulator-controllerGo / K8s operatorEmbeds install script call in VM cloud-init metadata with UAV_ID env var; knows each env’s Inventory Service URL

Integration Points

New Inventory Service endpoints:

EndpointMethodAuthPurpose
GET /installGETNoneServes the bash install script; Avatar URL and CA endpoint baked in from this instance’s deployment config
GET /internal/install/{uav_id}/certs/{file}GETShort-lived token (issued post-approval)Serves individual cert files (ca.crt, client.crt, client.key)

No env parameter on any endpoint — the Inventory Service instance is the environment.

Inventory Service deployment config (Helm values / env vars):

These values are already configured per deployment. The /install endpoint reads them to generate the script:

Config keyExample valuePurpose
AVATAR_ENDPOINTavatar.client-acme.droneup.cloud:9091Written into config.env on the UAV
CERT_DOMAIN_TEMPLATE{UAVID}.uav.client-acme.droneup.cloudUsed during cert issuance
MAVLINK_SHIM_VERSIONv2.1.0Binary version the script downloads
AUTO_APPROVEfalseIf true, UAVs approved instantly on registration
CA_CERT / CA_KEY(from Secret Manager)Used to sign UAV client certs

MAVLink Shim CI/CD — new publishing step:

  • GitHub Actions in uncrew-mavlink-shim must publish versioned mavlink_shim binaries for both aarch64 and x86_64 to Google Artifact Registry on each release tag.
  • The Inventory Service reads MAVLINK_SHIM_VERSION from its deployment config to populate the download URL in the script.

Data Model Changes

New install_tokens table in Inventory Service:

ColumnTypeNotes
idUUIDPK, used as bearer token
uav_idTEXTFK → registrations.uav_id
expires_atTIMESTAMPTZ1 h from issuance
used_atTIMESTAMPTZNullable; token is single-use

Token is issued only after the UAV transitions to APPROVED. No env column — this table lives inside a single env’s Inventory Service database.

No other schema changes are required. The existing registrations and certificates tables are already scoped to the deployment — no env_slug FK needed.

Install Script Structure

The script is generated at request time by the Inventory Service from its own deployment config. It is the same script regardless of which UAV runs it — UAV identity is resolved at runtime. Two modes:

  • Interactive (physical UAV): operator runs it in a shell; script detects hardware serial or prompts.
  • Headless (simulator): UAV_ID env var is set by the Simulator Controller; all prompts skipped.
#!/usr/bin/env bash
set -euo pipefail

# Values injected by the Inventory Service at request time (from its own deployment config):
INVENTORY_URL="https://inventory.client-acme.droneup.cloud"
AVATAR_URL="avatar.client-acme.droneup.cloud:9091"
VERSION="v2.1.0"

ARCH="$(uname -m)"  # aarch64 (Skynode) or x86_64 (simulator VM)

# 1. Resolve UAV ID — headless or interactive
if [[ -n "${UAV_ID:-}" ]]; then
  echo "Headless mode: UAV_ID=${UAV_ID}"
else
  if [ ! -t 0 ]; then
    echo "ERROR: UAV_ID env var must be set when running non-interactively." >&2
    exit 1
  fi
  SERIAL="$(cat /sys/firmware/devicetree/base/serial-number 2>/dev/null | tr -d '\0' || true)"
  if [[ -n "${SERIAL}" ]]; then
    read -rp "Detected UAV ID: ${SERIAL}. Continue? [Y/n] " confirm
    [[ "${confirm,,}" != "n" ]] && UAV_ID="${SERIAL}"
  fi
  if [[ -z "${UAV_ID:-}" ]]; then
    read -rp "Enter UAV ID: " UAV_ID
  fi
fi

# 2. Register UAV (idempotent — proceeds immediately if already APPROVED)
STATUS=$(curl -fsSL -X POST "${INVENTORY_URL}/v1/register" \
  -d "{\"uav_id\":\"${UAV_ID}\"}" | jq -r '.status')

# 3. Poll for approval (instant if AUTO_APPROVE=true on this instance)
if [[ "${STATUS}" != "APPROVED" ]]; then
  echo "Registration submitted. Waiting for approval..."
  for i in $(seq 1 180); do  # 30 min at 10 s intervals
    sleep 10
    STATUS=$(curl -fsSL "${INVENTORY_URL}/v1/uav/${UAV_ID}/status" | jq -r '.status')
    [[ "${STATUS}" == "APPROVED" ]] && break
    [[ "${STATUS}" == "REJECTED" ]] && { echo "Registration rejected."; exit 1; }
    echo "  still waiting... (${i})"
  done
  [[ "${STATUS}" != "APPROVED" ]] && { echo "Approval timeout."; exit 1; }
fi

# 4. Fetch cert bundle
TOKEN=$(curl -fsSL "${INVENTORY_URL}/v1/uav/${UAV_ID}/install-token" | jq -r '.token')
mkdir -p /etc/mavlink-shim/certs
set +x  # suppress key material from trace logs
for f in ca.crt client.crt client.key; do
  curl -fsSL -H "Authorization: Bearer ${TOKEN}" \
    "${INVENTORY_URL}/internal/install/${UAV_ID}/certs/${f}" \
    -o "/etc/mavlink-shim/certs/${f}"
  chmod 0600 "/etc/mavlink-shim/certs/${f}"
done
set -x

# 5. Download binary
curl -fsSL "https://us-east1-docker.pkg.dev/.../mavlink_shim_${VERSION}_${ARCH}" \
  -o /tmp/mavlink_shim
[[ -f /usr/bin/mavlink_shim ]] && cp /usr/bin/mavlink_shim /usr/bin/mavlink_shim.bak
install -m 0755 /tmp/mavlink_shim /usr/bin/mavlink_shim

# 6. Write config
cat > /etc/mavlink-shim/config.env <<EOF
MAVLINK_SHIM_AVATAR_URL=${AVATAR_URL}
MAVLINK_SHIM_ROOT_CERT_PATH=/etc/mavlink-shim/certs/ca.crt
MAVLINK_SHIM_CLIENT_CERT_PATH=/etc/mavlink-shim/certs/client.crt
MAVLINK_SHIM_CLIENT_KEY_PATH=/etc/mavlink-shim/certs/client.key
EOF

# 7. Install systemd unit + start
systemctl enable --now mavlink-shim
echo "mavlink-shim ${VERSION} installed, uav=${UAV_ID}"

Security & Privacy

ConcernMitigation
Install script URL is unauthenticatedScript contains no secrets — values baked in are non-sensitive (endpoint URLs, version). Cert bundle is only served after approval via a token-authenticated endpoint.
Private key in transitCert bundle fetched over HTTPS with bearer token; key never in URL params
Private key at rest on UAVWritten to /etc/mavlink-shim/certs/ with 0600 root-only permissions
Private key in script outputset +x before cert fetch loop; key content never echoed
CA private key in Inventory ServiceMounted from Google Secret Manager at deploy time; never in the database or logs
Cross-tenant isolationEach client’s Inventory Service is a separate deployment with separate Secret Manager secrets — infrastructure-level isolation, not application-level ACLs
AUTO_APPROVE on wrong instanceDeployment pipeline enforces AUTO_APPROVE=false for staging/prod/client Helm values; sandbox/dev values are separate files with explicit opt-in
Binary integrityMVP: downloaded from trusted Artifact Registry over HTTPS. v1.1: SHA256 checksum in script

5. Risks & Phased Rollout

Technical Risks

RiskLikelihoodImpactMitigation
Simulator VM is x86_64, not aarch64CertainMediumScript uses uname -m to select binary; Artifact Registry must publish both architectures
curl | bash blocked by UAV OS security policyLowHighInvestigate UAV OS policy before committing to this pattern; fallback: download script manually, verify, run
CA private key misconfigured in Secret Manager for a new client envLowHighDeployment checklist; automated smoke test that generates a cert and verifies chain before marking env ready
Simulator VM cloud-init networking not ready when script runsMediumMediumScript retries Inventory Service calls with exponential back-off before failing
AUTO_APPROVE accidentally set in a non-sandbox Helm values fileLowHighCI lint step rejects AUTO_APPROVE=true in any values file not named values-sandbox.yml or values-dev.yml

Phased Rollout

MVP

  • GET /install endpoint on Inventory Service — generates script from deployment config
  • GET /internal/install/{uav_id}/certs/{file} — serves cert bundle post-approval with token auth
  • install_tokens table — single-use, 1 h TTL
  • AUTO_APPROVE deployment config flag
  • Install script: interactive + headless modes, uname -m binary selection, cert write, config.env, systemd unit
  • Inventory Service UI: “Get install command” one-liner with copy button; auto-approve warning banner
  • uncrew-mavlink-shim CI: publish versioned aarch64 and x86_64 binaries to Artifact Registry
  • Simulator Controller: embed install script call in VM cloud-init metadata with UAV_ID env var

v1.1

  • SHA256 checksum verification of binary in install script
  • AI Node cert bundle support (second cert path set in script)
  • Install audit log (UAV ID, version, timestamp, token ID) written to Inventory Service DB
  • Cert-only refresh: re-run script to rotate certs without reinstalling binary

v2.0

  • Real-time install status visible in UI (requires telemetry from UAV post-install)
  • Binary version pinning in UI (select specific mavlink_shim version to deploy)
  • Automatic cert renewal before expiry

Dependencies

DependencyOwnerRisk
uncrew-mavlink-shim publishes versioned aarch64 + x86_64 binaries to Artifact RegistryMAVLink Shim teamBlocking for MVP
GET /install endpoint on Inventory ServiceInventory teamBlocking for MVP
Apollo UI “Get install command” sectionFrontend teamBlocking for MVP
Simulator Controller updated to embed install script call + UAV_IDSimulator teamBlocking for simulator path in MVP
Legal/safety review of 44807 implicationsSafety teamRequired before production client deployments

6. Estimation Input

prd_sizing_input:
  feature: "onboard-software-deployment"
  scope_clarity: "A"
  key_terms:
    - "mavlink-shim"
    - "install-script"
    - "certificate"
    - "inventory"
    - "simulator"
    - "cloud-init"
    - "auto-approve"
  risk_flags:
    - "security-certs"
    - "cross-repo"
    - "firmware-adjacent"
    - "multi-arch-binary"
  affected_repos:
    - "uncrew-inventory-service"
    - "uncrew-mavlink-shim"
    - "uncrew-apollo-frontend"
    - "uncrew-simulator-controller"
  domains:
    - "backend-go"
    - "frontend-react"
    - "firmware-cpp"
    - "infrastructure-gcp"
  regulatory: true
  discovery_needed: false
Last updated on