Package onboard K1 separately with a private portable installer

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 11:43:24 +03:00
parent 111fe3dcfb
commit d8cc5367c4
30 changed files with 1166 additions and 178 deletions
@@ -0,0 +1,6 @@
// Only WLAN discovery. No host association/profile modification authority.
polkit.addRule(function(action, subject) {
if (subject.user === "mission-core-k1" && action.id === "org.freedesktop.NetworkManager.wifi.scan") {
return polkit.Result.YES;
}
});
+79
View File
@@ -0,0 +1,79 @@
# Onboard K1 distribution
The accepted target is the owner's current activated K1, FW 3.0.2, on Ubuntu
24.04 amd64. Another physical scanner or firmware is not included in this
acceptance. The same prepared installer can be used on another supported
onboard host without a Mac, Keychain export, new packet capture or typed key.
`mission-core-node` 0.8.0 contains the host, shared SDK, existing RealSense
support and statically composed sensor UI. `mission-core-xgrids-k1` is an
optional package containing the K1 service, its reviewed Python dependencies,
platform adapters and application-material preparation. The optional package
depends on the host; the host does not depend on the optional package.
The old 0.7.1 monolithic installation is migrated using Debian `Breaks` and
`Replaces`. Existing filesystem paths, device identity, encrypted material,
recordings and physical-command journals remain in place. Package scripts
require a safe idle state and stop the worker before replacing its modules.
Removing the optional package preserves recordings, journals and material.
## Private autonomous installer
The public code package contains no key. The private edition
`0.1.0+private.1` carries the reviewed application material in one root-owned
mode-0600 member. The resulting `.deb` is itself private (mode 0600); distribute
it only as the owner's prepared installer, never through Git or a public
package registry. File permissions on the installed member do not encrypt the
distributable archive. This is a deliberate private distribution boundary,
not a claim that a client application's embedded material is unextractable.
At installation, the fixed root-owned helper reads that member and uses
`systemd-creds --with-key=host` to encrypt it for the destination computer.
The worker gets only `LoadCredentialEncrypted=k1-application`. Reinstalling
identical material is idempotent; a different existing value fails before
replacement and requires explicit rotation. There is no plaintext runtime
fallback, browser secret, environment variable, cloud service or per-scanner
key derivation. The administrative protected-stdin importer remains supported.
Building the private edition reads the material only from protected stdin:
```text
python plugins/xgrids-k1/packaging/build_deb.py
--wheel-root <reviewed-wheel-cache>
--output <private-output-directory>/mission-core-xgrids-k1_0.1.0+private.1_amd64.deb
--private-authority-stdin
```
This is an engineering build interface, not an operator prompt to type the key.
An authorized build can read the existing fixed Keychain item once and supply
those bytes in memory. No key value or key fingerprint enters public provenance.
The build refuses to overwrite an existing artifact. Destination paths and
credential values do not occur in source/runtime manifests.
Python wheels and application material are included. A fresh Ubuntu machine
may still need its normal package repositories for declared OS dependencies;
autonomous credential preparation does not claim an air-gapped OS installer.
## Runtime composition boundary
`runtime-files.json` declares the complete onboard Python import closure (85
modules at this increment). The builder rejects changed imports until the list
is reviewed. It includes shared session/viewer contracts that the existing K1
runtime still consumes; it does not pretend those dependencies have disappeared.
`composition.py` owns local Core/LAB router and archive composition. Its factory
was moved from the facade without changing its AST. The onboard worker does not
import it. Laboratory public exports are lazy to keep a leaf migration check
from loading all laboratory runners on the board.
This finishes a separately installable onboard package boundary, not the entire
device-plugin architecture. The local Core backend remains in-process, frontend
composition remains static, and the internal K1 facade still requires further
decomposition. No network protocol, recovery transition, START/STOP transcript,
camera producer or LAB/recorded/live Rerun settings were changed here.
The source for application-level identity is the retained LixelGO static/wire
analysis in `docs/05_K1_MQTT_STREAM_PROFILE.md` and ADR 0012. It distinguishes the
application value from the scanner's live vendor ID and serial. Current vendor
release notes also document a protocol change at FW 3.0.2 paired with LixelGO
1.2.0; software-version compatibility must therefore remain explicit:
https://www.xgrids.com/intl/support/download?page=K1
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""Build the optional onboard K1 integration, including a private autonomous edition.
The public build contains no application material. --private-authority-stdin
creates a mode-0600 private installer; the key never appears in arguments,
provenance, stdout or a source file. This installer itself is a private artifact.
"""
import argparse
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
PACKAGING = Path(__file__).resolve().parent
REPOSITORY = PACKAGING.parents[2]
sys.path.insert(0, str(REPOSITORY / "scripts/packaging"))
from credential_install import PROFILE_ID, validate # noqa: E402
from debian import package # noqa: E402
from runtime_payload import files as runtime_files # noqa: E402
VERSION = "0.1.0"
RESOURCES = (
"plugins/xgrids-k1/profile_loader.py",
"plugins/xgrids-k1/plugin.manifest.json",
"config/observatory-equipment-models.json",
"config/observatory-recorded-capture-profiles.json",
"plugins/xgrids-k1/profiles/fw-3.0.2/local-network.v2.json",
)
def payload(wheel_root, *, authority=None):
files = []
for name in ("k1_prepare.py", "k1_bootstrap.py", "install-k1-credential"):
files.append(
(
"usr/lib/mission-core-node/" + name,
(PACKAGING / name).read_bytes(),
0o755 if name == "install-k1-credential" else 0o644,
)
)
files.extend(
[
(
"usr/lib/mission-core-node/k1-install/credential_install.py",
(PACKAGING / "credential_install.py").read_bytes(),
0o644,
),
(
"usr/lib/mission-core-node/prepare-k1-profile",
(PACKAGING / "prepare-bundled-profile").read_bytes(),
0o755,
),
(
"usr/lib/systemd/system/mission-core-k1.service",
(PACKAGING / "mission-core-k1.service").read_bytes(),
0o644,
),
(
"usr/share/polkit-1/rules.d/50-mission-core-k1.rules",
(PACKAGING / "50-mission-core-k1.rules").read_bytes(),
0o644,
),
(
"usr/share/mission-core-node/k1/bundle.json",
(PACKAGING / "k1-bundle.json").read_bytes(),
0o644,
),
]
)
wheels = json.loads((PACKAGING / "k1-bundle.json").read_text())["wheels"]
for item in wheels:
path = wheel_root / item["name"]
if path.name != item["name"] or path.is_symlink():
raise ValueError("Unsafe K1 wheel path")
data = path.read_bytes()
if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("K1 wheel differs from the admitted bundle")
files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644))
sources = runtime_files()
for path in sources:
files.append(
(
"usr/lib/mission-core-node/k1/" + str(path.relative_to(REPOSITORY)),
path.read_bytes(),
0o644,
)
)
for relative in RESOURCES:
files.append(
(
"usr/lib/mission-core-node/k1/" + relative,
(REPOSITORY / relative).read_bytes(),
0o644,
)
)
# Hash only code and public metadata. Never hash private material into a
# public per-file manifest or represent it as scanner identity.
provenance = {
"package": "mission-core-xgrids-k1",
"version": VERSION + ("+private.1" if authority is not None else ""),
"base_commit": subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=REPOSITORY, text=True
).strip(),
"application_profile": PROFILE_ID,
"application_material_included": authority is not None,
"acceptance_scope": "owner-controlled-k1-fw-3.0.2-ubuntu-24.04-amd64",
"files": {name: hashlib.sha256(data).hexdigest() for name, data, _ in files},
}
files.append(
(
"usr/share/doc/mission-core-xgrids-k1/provenance.json",
(json.dumps(provenance, indent=2) + "\n").encode(),
0o644,
)
)
if authority is not None:
validate(authority)
files.append(
("usr/share/mission-core-node/k1/private-application-key", bytes(authority), 0o600)
)
return files
def build(destination, wheel_root, *, authority=None):
if authority is not None:
validate(authority)
version = VERSION + ("+private.1" if authority is not None else "")
control = f"""Package: mission-core-xgrids-k1
Version: {version}
Architecture: amd64
Maintainer: NODE.DC local build <noreply@example.invalid>
Section: admin
Priority: optional
Depends: mission-core-node (>= 0.8.0), mission-core-node (<< 0.9.0),
systemd, python3, adduser, bluez, network-manager, iproute2, ffmpeg
Breaks: mission-core-node (<< 0.8.0)
Replaces: mission-core-node (<< 0.8.0)
Description: Optional XGRIDS K1 Bridge integration for Mission Core Node
Reviewed FW 3.0.2 runtime with a separate process and durable device journals.
""".encode()
controls = [("control", control, 0o644)]
controls += [
(name, (PACKAGING / name).read_bytes(), 0o755)
for name in ("preinst", "postinst", "prerm", "postrm")
]
archive = package(controls, payload(wheel_root, authority=authority))
destination.parent.mkdir(parents=True, exist_ok=True)
# Never replace a previously published release with different bytes.
fd, staged = tempfile.mkstemp(prefix=".k1-package-", dir=destination.parent)
try:
with os.fdopen(fd, "wb") as stream:
os.fchmod(stream.fileno(), 0o600 if authority is not None else 0o644)
stream.write(archive)
stream.flush()
os.fsync(stream.fileno())
os.link(staged, destination)
finally:
os.unlink(staged)
return {
"package": destination.name,
"version": version,
"bytes": len(archive),
"sha256": hashlib.sha256(archive).hexdigest(),
"private": authority is not None,
}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--wheel-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--private-authority-stdin", action="store_true")
args = parser.parse_args()
secret = None
try:
if args.private_authority_stdin:
secret = bytearray(sys.stdin.buffer.read(1025).strip())
print(json.dumps(build(args.output, args.wheel_root, authority=secret)))
except Exception:
raise SystemExit("K1 package build failed; check inputs and unused output path") from None
finally:
if secret is not None:
secret[:] = b"\0" * len(secret)
@@ -0,0 +1,127 @@
"""Root-only preparation of the private application material shipped with K1.
An autonomous private installer supplies one fixed, root-readable file. The
worker receives only a systemd credential encrypted on this board. Reinstalling
the same material is harmless; changing it requires explicit rotation.
"""
import hmac
import os
import stat
import subprocess
import tempfile
from pathlib import Path
PROFILE_ID = "lixelgo.application.k1-fw-3.0.2.v1"
STORE = Path("/etc/credstore.encrypted")
BUNDLE = Path("/usr/share/mission-core-node/k1/private-application-key")
class CredentialInstallError(ValueError):
"""Secret-free failure; input and subprocess diagnostics never escape."""
def validate(secret):
if len(secret) != 36 or any(v < 33 or v > 126 for v in secret):
raise CredentialInstallError("Invalid material for the reviewed K1 profile")
def read_bundle(path=BUNDLE):
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, "rb") as stream:
info = os.fstat(stream.fileno())
if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o077:
raise CredentialInstallError(
"Private material must be a root-owned private regular file"
)
secret = bytearray(stream.read(1025))
try:
validate(secret)
return secret
except CredentialInstallError:
secret[:] = b"\0" * len(secret)
raise
def install(secret, *, root=STORE, runner=subprocess.run):
if os.geteuid() != 0:
raise CredentialInstallError("Administrator authentication required")
validate(secret)
root.mkdir(mode=0o700, exist_ok=True)
if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022:
raise CredentialInstallError("Unsafe credential store")
path = root / "k1-application"
if path.is_symlink():
raise CredentialInstallError("Unsafe credential target")
if path.exists():
info = path.stat()
if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o077:
raise CredentialInstallError("Unsafe installed credential")
result = runner(
["/usr/bin/systemd-creds", "decrypt", "--name=k1-application", str(path), "-"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
timeout=30,
check=False,
)
current = bytearray(result.stdout)
try:
if result.returncode or not hmac.compare_digest(current, secret):
raise CredentialInstallError(
"Existing material differs; explicit rotation required"
)
return "unchanged"
finally:
current[:] = b"\0" * len(current)
with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory:
staged = Path(directory) / "encrypted"
result = runner(
[
"/usr/bin/systemd-creds",
"encrypt",
"--name=k1-application",
"--with-key=host",
"-",
str(staged),
],
input=secret,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=30,
check=False,
)
if result.returncode:
raise CredentialInstallError("Application material preparation failed")
staged.chmod(0o600)
with staged.open("rb") as stream:
os.fsync(stream.fileno())
os.link(staged, path)
directory_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
return "installed"
def main(*, bundled=False):
import sys
secret = bytearray()
try:
if os.geteuid() != 0 or len(sys.argv) != 1:
raise CredentialInstallError("Administrator-only preparation required")
if bundled:
if not BUNDLE.exists() and not BUNDLE.is_symlink():
return
secret = read_bundle()
else:
secret = bytearray(sys.stdin.buffer.read(1025).strip())
install(secret)
except (OSError, ValueError, subprocess.SubprocessError):
raise SystemExit(
"K1 application profile was not installed; "
"check the private release or existing profile"
) from None
finally:
secret[:] = b"\0" * len(secret)
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
mc_release_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd "$mc_release_dir"
/usr/bin/sha256sum --check SHA256SUMS
mc_node_package="$mc_release_dir/mission-core-node_0.8.0_amd64.deb"
mc_k1_package="$mc_release_dir/mission-core-xgrids-k1_0.1.0+private.1_amd64.deb"
if [ -t 0 ]; then
exec /usr/bin/sudo /usr/bin/apt-get install -y "$mc_node_package" "$mc_k1_package"
fi
exec /usr/bin/gnome-terminal --wait --title="Mission Core · K1" -- \
/usr/bin/sudo /usr/bin/apt-get install -y "$mc_node_package" "$mc_k1_package"
@@ -0,0 +1,7 @@
#!/usr/bin/python3 -I
import sys
sys.path.insert(0, "/usr/lib/mission-core-node/k1-install")
from credential_install import main
main()
+265
View File
@@ -0,0 +1,265 @@
{
"schema": "missioncore.node.driver-bundle/v1",
"model_id": "xgrids.k1",
"revision": "c7ed0bba39f757afdac176a8",
"python": "3.12",
"platform": "linux-amd64",
"lock_sha256": "551c8ccdc44bc3724328dd1e316c81d20e63d2e4dcd97148377d6cacef479246",
"wheels": [
{
"name": "aioice-0.10.2-py3-none-any.whl",
"sha256": "14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf",
"bytes": 24875
},
{
"name": "aiortc-1.14.0-py3-none-any.whl",
"sha256": "4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e",
"bytes": 93183
},
{
"name": "annotated_doc-0.0.4-py3-none-any.whl",
"sha256": "571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320",
"bytes": 5303
},
{
"name": "annotated_types-0.7.0-py3-none-any.whl",
"sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
"bytes": 13643
},
{
"name": "anyio-4.14.2-py3-none-any.whl",
"sha256": "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494",
"bytes": 125813
},
{
"name": "attrs-26.1.0-py3-none-any.whl",
"sha256": "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309",
"bytes": 67548
},
{
"name": "av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl",
"sha256": "7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2",
"bytes": 41174337
},
{
"name": "bleak-3.0.2-py3-none-any.whl",
"sha256": "39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d",
"bytes": 146490
},
{
"name": "certifi-2026.7.22-py3-none-any.whl",
"sha256": "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775",
"bytes": 136983
},
{
"name": "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"sha256": "c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf",
"bytes": 221822
},
{
"name": "click-8.4.2-py3-none-any.whl",
"sha256": "e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76",
"bytes": 119243
},
{
"name": "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl",
"sha256": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b",
"bytes": 4459756
},
{
"name": "dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9",
"bytes": 852687
},
{
"name": "dnspython-2.8.0-py3-none-any.whl",
"sha256": "01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af",
"bytes": 331094
},
{
"name": "fastapi-0.139.0-py3-none-any.whl",
"sha256": "cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189",
"bytes": 130339
},
{
"name": "foxglove_sdk-0.25.3-cp310-abi3-manylinux_2_28_x86_64.whl",
"sha256": "bcc894b88188d8169973cfbb1370300f671760adea9d6e9447e5a03b2289527d",
"bytes": 19220466
},
{
"name": "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl",
"sha256": "14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411",
"bytes": 33364
},
{
"name": "h11-0.16.0-py3-none-any.whl",
"sha256": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86",
"bytes": 37515
},
{
"name": "httpcore-1.0.9-py3-none-any.whl",
"sha256": "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55",
"bytes": 78784
},
{
"name": "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
"sha256": "b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2",
"bytes": 523851
},
{
"name": "httpx-0.28.1-py3-none-any.whl",
"sha256": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad",
"bytes": 73517
},
{
"name": "idna-3.18-py3-none-any.whl",
"sha256": "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2",
"bytes": 65455
},
{
"name": "ifaddr-0.2.0-py3-none-any.whl",
"sha256": "085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748",
"bytes": 12314
},
{
"name": "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e",
"bytes": 1368249
},
{
"name": "markdown_it_py-4.2.0-py3-none-any.whl",
"sha256": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a",
"bytes": 91687
},
{
"name": "mdurl-0.1.2-py3-none-any.whl",
"sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
"bytes": 9979
},
{
"name": "numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca",
"bytes": 16672469
},
{
"name": "paho_mqtt-2.1.0-py3-none-any.whl",
"sha256": "6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee",
"bytes": 67219
},
{
"name": "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91",
"bytes": 6940830
},
{
"name": "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9",
"bytes": 155560
},
{
"name": "pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl",
"sha256": "5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778",
"bytes": 50088993
},
{
"name": "pycparser-3.0-py3-none-any.whl",
"sha256": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992",
"bytes": 48172
},
{
"name": "pydantic-2.13.4-py3-none-any.whl",
"sha256": "45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba",
"bytes": 472262
},
{
"name": "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"sha256": "926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce",
"bytes": 2094516
},
{
"name": "pyee-14.0.0-py3-none-any.whl",
"sha256": "3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d",
"bytes": 15553
},
{
"name": "pygments-2.20.0-py3-none-any.whl",
"sha256": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176",
"bytes": 1231151
},
{
"name": "pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc",
"bytes": 2434534
},
{
"name": "pyopenssl-26.2.0-py3-none-any.whl",
"sha256": "4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70",
"bytes": 55823
},
{
"name": "python_dotenv-1.2.2-py3-none-any.whl",
"sha256": "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a",
"bytes": 22101
},
{
"name": "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
"bytes": 807870
},
{
"name": "rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl",
"sha256": "287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe",
"bytes": 163018185
},
{
"name": "rich-14.3.4-py3-none-any.whl",
"sha256": "07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952",
"bytes": 310480
},
{
"name": "shellingham-1.5.4-py2.py3-none-any.whl",
"sha256": "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
"bytes": 9755
},
{
"name": "starlette-1.3.1-py3-none-any.whl",
"sha256": "c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6",
"bytes": 73632
},
{
"name": "typer-0.26.8-py3-none-any.whl",
"sha256": "3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c",
"bytes": 122564
},
{
"name": "typing_extensions-4.16.0-py3-none-any.whl",
"sha256": "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8",
"bytes": 45571
},
{
"name": "typing_inspection-0.4.2-py3-none-any.whl",
"sha256": "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7",
"bytes": 14611
},
{
"name": "uvicorn-0.51.0-py3-none-any.whl",
"sha256": "5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b",
"bytes": 73219
},
{
"name": "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4",
"bytes": 4426307
},
{
"name": "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"sha256": "e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5",
"bytes": 456398
},
{
"name": "websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
"sha256": "35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a",
"bytes": 187345
}
]
}
@@ -0,0 +1,26 @@
"""Fixed root-owned import path; no user site, environment path or import hooks."""
import os
import sys
from pathlib import Path
root = Path("/var/lib/mission-core-k1-runtime")
reference = root / "active.path"
runtime = Path(reference.read_text().strip())
if (
reference.is_symlink()
or runtime.is_symlink()
or runtime.parent != root
or not runtime.name.isalnum()
or runtime.stat().st_uid != 0
or runtime.stat().st_mode & 0o022
):
raise RuntimeError("Unsafe K1 runtime")
sys.path[:0] = [
str(runtime), str(runtime / "rerun_sdk"),
"/usr/lib/mission-core-node/k1/src", "/usr/lib/mission-core-node/sdk",
]
os.environ["MISSIONCORE_DATA_DIR"] = "/var/lib/mission-core-k1"
from k1link.device_plugins.xgrids_k1.node_bridge import main
main()
+94
View File
@@ -0,0 +1,94 @@
"""Install only the bundled, hash-pinned Ubuntu K1 runtime; no network I/O."""
import hashlib
import json
import os
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path, PurePosixPath
SHARE = Path("/usr/share/mission-core-node/k1")
ROOT = Path("/var/lib/mission-core-k1-runtime")
def members(archive):
for info in archive.infolist():
path = PurePosixPath(info.filename)
if (
path.is_absolute()
or ".." in path.parts
or (info.external_attr >> 16) & 0o170000 == 0o120000
or ".data" in path.parts
):
raise RuntimeError("Unsafe K1 runtime archive")
# Rerun's pinned wheel declares this one static package directory.
# We do not execute .pth files; bootstrap adds the exact directory.
if info.filename.endswith(".pth") and not (
info.filename == "rerun_sdk.pth" and archive.read(info) == b"rerun_sdk\n"
):
raise RuntimeError("Unreviewed K1 Python path hook")
# Distribution script/data relocation must be handled deliberately,
# never interpreted as an install hook by the operator's Python.
if any(part.endswith(".data") for part in path.parts):
raise RuntimeError("K1 wheel requires unsupported relocation")
yield info
def prepare():
if os.geteuid() != 0 or os.uname().machine != "x86_64" or sys.version_info[:2] != (3, 12):
raise RuntimeError("K1 runtime requires privileged Ubuntu amd64 Python 3.12 installation")
release = Path("/etc/os-release").read_text()
if "ID=ubuntu" not in release or 'VERSION_ID="24.04"' not in release:
raise RuntimeError("K1 runtime requires Ubuntu 24.04")
manifest = json.loads((SHARE / "bundle.json").read_text())
revision = manifest["revision"]
if not revision.isalnum():
raise RuntimeError("Invalid K1 runtime revision")
ROOT.mkdir(mode=0o755, exist_ok=True)
if ROOT.is_symlink() or ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022:
raise RuntimeError("Unsafe K1 runtime root")
target = ROOT / revision
if target.is_symlink() or (ROOT / "active.path").is_symlink():
raise RuntimeError("Unsafe K1 runtime reference")
for item in manifest["wheels"]:
path = SHARE / item["name"]
if (
path.name != item["name"]
or path.is_symlink()
or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]
):
raise RuntimeError("K1 runtime checksum mismatch")
if not target.exists():
stage = Path(tempfile.mkdtemp(prefix=".k1-", dir=ROOT))
try:
for item in manifest["wheels"]:
with zipfile.ZipFile(SHARE / item["name"]) as archive:
archive.extractall(stage, members=members(archive))
for path in stage.rglob("*"):
path.chmod(0o755 if path.is_dir() else 0o644)
stage.chmod(0o755)
stage.rename(target)
finally:
if stage.exists():
shutil.rmtree(stage)
for item in manifest["wheels"]:
with zipfile.ZipFile(SHARE / item["name"]) as archive:
for info in members(archive):
path = target / info.filename
if path.is_symlink() or (
not info.is_dir() and path.read_bytes() != archive.read(info)
):
raise RuntimeError("Installed K1 runtime differs from bundled wheel")
fd, name = tempfile.mkstemp(prefix=".active-", dir=ROOT)
with os.fdopen(fd, "w") as stream:
os.fchmod(stream.fileno(), 0o644)
stream.write(str(target))
stream.flush()
os.fsync(stream.fileno())
os.replace(name, ROOT / "active.path")
if __name__ == "__main__":
prepare()
@@ -0,0 +1,38 @@
[Unit]
Description=Mission Core Node K1 Bridge and acquisition
After=bluetooth.service NetworkManager.service
Wants=bluetooth.service NetworkManager.service
[Service]
Type=simple
User=mission-core-k1
Group=mission-core-node
SupplementaryGroups=bluetooth
ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/k1_bootstrap.py
StateDirectory=mission-core-k1
StateDirectoryMode=0700
RuntimeDirectory=mission-core-k1
RuntimeDirectoryMode=0750
LoadCredentialEncrypted=k1-application
UMask=0007
Environment=OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2
Restart=on-failure
RestartSec=3
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
CapabilityBoundingSet=
LockPersonality=yes
TasksMax=128
MemoryMax=2G
LimitNOFILE=2048
[Install]
WantedBy=multi-user.target
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
set -eu
case "$1" in
configure)
if ! getent passwd mission-core-k1 >/dev/null; then
adduser --system --home /var/lib/mission-core-k1 --no-create-home --disabled-login --ingroup mission-core-node mission-core-k1
fi
/usr/bin/python3 -I /usr/lib/mission-core-node/k1_prepare.py
/usr/lib/mission-core-node/prepare-k1-profile
if [ -d /run/systemd/system ]; then
systemctl daemon-reload
systemctl enable mission-core-k1.service
systemctl restart mission-core-k1.service
fi
;;
esac
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
if [ -d /run/systemd/system ]; then
systemctl daemon-reload
fi
# Removal preserves recordings, physical-command journals and encrypted
# application material. Package removal is not device state reset or rotation.
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
set -eu
case "$1" in
install|upgrade)
. /etc/os-release
if [ "${ID:-}" != ubuntu ] || [ "${VERSION_ID:-}" != 24.04 ]; then
echo "K1 integration requires Ubuntu 24.04 amd64." >&2
exit 1
fi
if [ -d /run/systemd/system ]; then
if [ -S /run/mission-core-k1/driver.sock ]; then
if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then
echo "Завершите подключение или запись K1 перед обновлением интеграции." >&2
exit 1
fi
fi
if [ -f /usr/lib/systemd/system/mission-core-k1.service ]; then
systemctl stop mission-core-k1.service
fi
fi
;;
esac
@@ -0,0 +1,7 @@
#!/usr/bin/python3 -I
import sys
sys.path.insert(0, "/usr/lib/mission-core-node/k1-install")
from credential_install import main
main(bundled=True)
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
if [ -d /run/systemd/system ]; then
if [ -S /run/mission-core-k1/driver.sock ]; then
if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then
echo "Завершите подключение или запись K1 перед удалением интеграции." >&2
exit 1
fi
fi
systemctl stop mission-core-k1.service
case "$1" in remove|deconfigure) systemctl disable mission-core-k1.service || true ;; esac
fi
@@ -0,0 +1,60 @@
"""Resolve Linux wheel inputs from the frozen monorepo lock, without downloading."""
import hashlib
import json
import subprocess
import tomllib
from pathlib import Path
from packaging.markers import default_environment
from packaging.requirements import Requirement
from packaging.tags import compatible_tags, cpython_tags
from packaging.utils import canonicalize_name, parse_wheel_filename
ROOT = Path(__file__).resolve().parents[1]
REPOSITORY = ROOT.parents[1]
def resolve():
requirements = ROOT / "build/k1-requirements.txt"
requirements.parent.mkdir(exist_ok=True)
subprocess.run(["uv", "export", "--frozen", "--extra", "node-device-media", "--no-dev",
"--no-emit-project", "--no-emit-package", "missioncore-plugin-sdk", "--no-hashes",
"--output-file", str(requirements)], cwd=REPOSITORY, check=True, stdout=subprocess.DEVNULL)
environment = default_environment()
environment.update(sys_platform="linux", platform_system="Linux", platform_machine="x86_64",
python_version="3.12", python_full_version="3.12.3", implementation_name="cpython",
platform_python_implementation="CPython")
platforms = [f"manylinux_2_{n}_x86_64" for n in range(39, 16, -1)] + ["manylinux2014_x86_64", "linux_x86_64"]
tags = list(cpython_tags((3, 12), platforms=platforms)) + list(compatible_tags((3, 12), interpreter="cp312", platforms=platforms))
ranks = {tag: i for i, tag in enumerate(tags)}
lock_data = (REPOSITORY / "uv.lock").read_bytes()
lock = tomllib.loads(lock_data.decode())
items = []
for line in requirements.read_text().splitlines():
if not line or line.lstrip().startswith("#"):
continue
requirement = Requirement(line)
if requirement.marker and not requirement.marker.evaluate(environment):
continue
name = canonicalize_name(requirement.name)
package = next(v for v in lock["package"] if canonicalize_name(v["name"]) == name and v["version"] in requirement.specifier)
candidates = []
for wheel in package.get("wheels", []):
filename = wheel["url"].split("/")[-1]
_, _, _, wheel_tags = parse_wheel_filename(filename)
matches = [ranks[tag] for tag in wheel_tags if tag in ranks]
if matches:
candidates.append((min(matches), filename, wheel))
if not candidates:
raise RuntimeError("No reviewed Linux wheel: " + name)
_, filename, wheel = min(candidates)
items.append({"name": filename, "sha256": wheel["hash"].removeprefix("sha256:"), "bytes": wheel["size"]})
revision = hashlib.sha256(json.dumps(items, sort_keys=True).encode()).hexdigest()[:24]
manifest = {"schema": "missioncore.node.driver-bundle/v1", "model_id": "xgrids.k1", "revision": revision,
"python": "3.12", "platform": "linux-amd64", "lock_sha256": hashlib.sha256(lock_data).hexdigest(), "wheels": items}
(ROOT / "packaging/k1-bundle.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(json.dumps({"wheels": len(items), "bytes": sum(v["bytes"] for v in items), "revision": revision}))
if __name__ == "__main__":
resolve()
@@ -0,0 +1,91 @@
{
"schema": "missioncore.plugin.python-payload/v1",
"entrypoint": "k1link.device_plugins.xgrids_k1.node_bridge",
"files": [
"src/k1link/__init__.py",
"src/k1link/artifact_gateway.py",
"src/k1link/artifacts.py",
"src/k1link/compute/__init__.py",
"src/k1link/compute/live_perception.py",
"src/k1link/data_plane/__init__.py",
"src/k1link/data_plane/views.py",
"src/k1link/device_plugins/__init__.py",
"src/k1link/device_plugins/xgrids_k1/__init__.py",
"src/k1link/device_plugins/xgrids_k1/active_acquisition_recovery_checkpoint.py",
"src/k1link/device_plugins/xgrids_k1/application_control_process_lease.py",
"src/k1link/device_plugins/xgrids_k1/ble/__init__.py",
"src/k1link/device_plugins/xgrids_k1/ble/ap_activation.py",
"src/k1link/device_plugins/xgrids_k1/ble/runtime_arbiter.py",
"src/k1link/device_plugins/xgrids_k1/ble/scanner.py",
"src/k1link/device_plugins/xgrids_k1/ble/wifi_provisioning.py",
"src/k1link/device_plugins/xgrids_k1/calibration_schema.py",
"src/k1link/device_plugins/xgrids_k1/calibration_snapshot.py",
"src/k1link/device_plugins/xgrids_k1/camera.py",
"src/k1link/device_plugins/xgrids_k1/connection_attempt.py",
"src/k1link/device_plugins/xgrids_k1/connection_supervisor.py",
"src/k1link/device_plugins/xgrids_k1/device_identity_pin_store.py",
"src/k1link/device_plugins/xgrids_k1/facade.py",
"src/k1link/device_plugins/xgrids_k1/firmware_credential.py",
"src/k1link/device_plugins/xgrids_k1/host_diagnostics.py",
"src/k1link/device_plugins/xgrids_k1/linux_host.py",
"src/k1link/device_plugins/xgrids_k1/live_perception_shadow.py",
"src/k1link/device_plugins/xgrids_k1/mqtt/__init__.py",
"src/k1link/device_plugins/xgrids_k1/mqtt/capture.py",
"src/k1link/device_plugins/xgrids_k1/network_mutation_ledger.py",
"src/k1link/device_plugins/xgrids_k1/network_provisioning_idempotency_journal.py",
"src/k1link/device_plugins/xgrids_k1/node_bridge.py",
"src/k1link/device_plugins/xgrids_k1/node_sensor.py",
"src/k1link/device_plugins/xgrids_k1/physical_command_coordinator.py",
"src/k1link/device_plugins/xgrids_k1/physical_command_ledger.py",
"src/k1link/device_plugins/xgrids_k1/protocol/__init__.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_acceptance.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_authority.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_bootstrap.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_execution.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_mqtt.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_publish.py",
"src/k1link/device_plugins/xgrids_k1/protocol/application_session.py",
"src/k1link/device_plugins/xgrids_k1/protocol/calibration_file.py",
"src/k1link/device_plugins/xgrids_k1/protocol/calibration_mqtt.py",
"src/k1link/device_plugins/xgrids_k1/protocol/modeling.py",
"src/k1link/device_plugins/xgrids_k1/protocol/modeling_control.py",
"src/k1link/device_plugins/xgrids_k1/protocol/modeling_safety.py",
"src/k1link/device_plugins/xgrids_k1/protocol/normalizer.py",
"src/k1link/device_plugins/xgrids_k1/protocol/protobuf_wire.py",
"src/k1link/device_plugins/xgrids_k1/protocol/streams.py",
"src/k1link/device_plugins/xgrids_k1/quick_connect_profile.py",
"src/k1link/device_plugins/xgrids_k1/semantic_topology_store.py",
"src/k1link/device_plugins/xgrids_k1/viewer/__init__.py",
"src/k1link/device_plugins/xgrids_k1/viewer/messages.py",
"src/k1link/device_plugins/xgrids_k1/viewer/replay.py",
"src/k1link/device_plugins/xgrids_k1/viewer/runtime.py",
"src/k1link/device_plugins/xgrids_k1/wifi_failure.py",
"src/k1link/host_network/__init__.py",
"src/k1link/host_network/wifi.py",
"src/k1link/laboratory/__init__.py",
"src/k1link/laboratory/canonical_recorded_migration.py",
"src/k1link/media_fragments.py",
"src/k1link/sessions/__init__.py",
"src/k1link/sessions/active.py",
"src/k1link/sessions/camera_frame.py",
"src/k1link/sessions/equipment.py",
"src/k1link/sessions/lab_cache.py",
"src/k1link/sessions/media.py",
"src/k1link/sessions/models.py",
"src/k1link/sessions/plugin_contract.py",
"src/k1link/sessions/preparation.py",
"src/k1link/sessions/recording.py",
"src/k1link/sessions/store.py",
"src/k1link/viewer/__init__.py",
"src/k1link/viewer/metrics.py",
"src/k1link/viewer/node_media.py",
"src/k1link/viewer/node_rerun.py",
"src/k1link/viewer/recorded.py",
"src/k1link/viewer/recorded_blueprint_lifecycle.py",
"src/k1link/viewer/rerun_bridge.py",
"src/k1link/web/__init__.py",
"src/k1link/web/camera_archive.py",
"src/k1link/web/device_lifecycle.py",
"src/k1link/web/plugin_runtime.py"
]
}
@@ -0,0 +1,95 @@
"""Reviewable import closure for the onboard plugin; no entire Core tree."""
import ast
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
MANIFEST = Path(__file__).with_name("runtime-files.json")
ENTRYPOINT = "k1link.device_plugins.xgrids_k1.node_bridge"
def runtime_nodes(node):
# A lazy package's typing-only reexports must not ship every LAB algorithm.
if isinstance(node, ast.If) and (
isinstance(node.test, ast.Name)
and node.test.id == "TYPE_CHECKING"
or isinstance(node.test, ast.Attribute)
and node.test.attr == "TYPE_CHECKING"
):
for child in node.orelse:
yield from runtime_nodes(child)
return
yield node
for child in ast.iter_child_nodes(node):
yield from runtime_nodes(child)
def import_closure(repository=ROOT):
source = repository / "src"
modules = {}
for path in (source / "k1link").rglob("*.py"):
name = ".".join(path.relative_to(source).with_suffix("").parts).removesuffix(".__init__")
modules[name] = path
seen, todo = set(), [ENTRYPOINT]
while todo:
name = todo.pop()
if name in seen:
continue
seen.add(name)
path = modules[name]
package = name if path.name == "__init__.py" else name.rpartition(".")[0]
imports = {".".join(name.split(".")[:i]) for i in range(1, len(name.split(".")))}
for node in runtime_nodes(ast.parse(path.read_text())):
if isinstance(node, ast.Import):
imports.update(a.name for a in node.names)
elif isinstance(node, ast.ImportFrom):
base = node.module or ""
if node.level:
prefix = ".".join(
package.split(".")[: len(package.split(".")) - node.level + 1]
)
base = prefix + ("." + base if base else "")
imports.add(base)
imports.update(base + "." + a.name for a in node.names)
todo.extend(sorted((imports & modules.keys()) - seen))
return sorted(str(modules[name].relative_to(repository)) for name in seen)
def files(repository=ROOT):
declared = json.loads(MANIFEST.read_text())
actual = import_closure(repository)
if (
declared["schema"] != "missioncore.plugin.python-payload/v1"
or declared["entrypoint"] != ENTRYPOINT
or actual != declared["files"]
):
raise ValueError("K1 runtime imports changed; review its payload manifest before packaging")
result = []
for name in actual:
path = repository / name
if path.is_symlink():
raise ValueError("Symlink in plugin source payload")
result.append(path)
return result
def provenance(repository=ROOT):
return {
str(p.relative_to(repository)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in files(repository)
}
if __name__ == "__main__":
print(
json.dumps(
{
"schema": "missioncore.plugin.python-payload/v1",
"entrypoint": ENTRYPOINT,
"files": import_closure(),
},
indent=2,
)
)
+1 -1
View File
@@ -9,7 +9,7 @@
"spec": {
"hostApiRange": "v1alpha2",
"runtime": {
"backendEntrypoint": "k1link.device_plugins.xgrids_k1.facade:build_xgrids_k1_plugin",
"backendEntrypoint": "k1link.device_plugins.xgrids_k1.composition:build_xgrids_k1_plugin",
"isolation": "transitional-in-process"
},
"compatibilityProfiles": [