chore(node): preserve pre-canonicalization experiment snapshot

Historical working copy retained for audit before consolidation into main. The canonicalized plugin architecture and later fixes already live in main; this snapshot is not a release or a request to restore obsolete source layout.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-21 08:45:34 +03:00
parent 020a878915
commit 1c7dd29d8a
89 changed files with 5026 additions and 487 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;
}
});
+3 -1
View File
@@ -11,7 +11,7 @@ import sys
from build_deb import build, VERSION, BRAND_SHA256
ROOT = Path(__file__).resolve().parents[1]
DG_COMMIT = "999864e5b0a81555823cfa1ea6e8cf8a417c37f1"
DG_COMMIT = "1bdfc6c24072d38cc1068086ea271c444f2524ad"
def guideline_sources():
@@ -32,6 +32,8 @@ def provenance():
"base_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
"design_guideline_commit": DG_COMMIT,
"design_guideline_files": guideline_sources(),
"shared_sensor_ui_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "packages/sensor-ui/src").rglob("*")) if p.is_file()},
"k1_runtime_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "src/k1link").rglob("*")) if p.is_file() and p.suffix in (".py", ".json")},
"toolchain": json.loads((ROOT / "toolchain.json").read_text()), "files": files}
+25 -2
View File
@@ -13,7 +13,7 @@ import tarfile
ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.6.11"
VERSION = "0.7.0"
BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af"
@@ -65,7 +65,7 @@ Architecture: amd64
Maintainer: NODE.DC local build <noreply@example.invalid>
Section: admin
Priority: optional
Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme
Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, bluez, network-manager, iproute2, ffmpeg
Description: Mission Core onboard computer configuration
Local graphical setup, host inventory, SSH access and persistent node identity.
""".encode()
@@ -107,6 +107,29 @@ Description: Mission Core onboard computer configuration
files.append(("usr/share/mission-core-node/realsense/" + item["name"], data, 0o644))
for path in (ROOT / "sensors").glob("*.py"):
files.append(("usr/lib/mission-core-node/sensors/" + path.name, path.read_bytes(), 0o644))
for name in ("k1_prepare.py", "k1_bootstrap.py"):
files.append(("usr/lib/mission-core-node/" + name, (p / name).read_bytes(), 0o644))
files.append(("usr/lib/mission-core-node/install-k1-credential", (p / "install-k1-credential").read_bytes(), 0o755))
files.append(("usr/lib/systemd/system/mission-core-k1.service", (p / "mission-core-k1.service").read_bytes(), 0o644))
files.append(("usr/share/polkit-1/rules.d/50-mission-core-k1.rules", (p / "50-mission-core-k1.rules").read_bytes(), 0o644))
k1_bundle = json.loads((p / "k1-bundle.json").read_text())
files.append(("usr/share/mission-core-node/k1/bundle.json", (p / "k1-bundle.json").read_bytes(), 0o644))
for item in k1_bundle["wheels"]:
data = (ROOT / "build/k1-wheels" / item["name"]).read_bytes()
if hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("K1 bundle hash mismatch")
files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644))
repository = ROOT.parents[1]
# Reuse the admitted plugin runtime and the transport-neutral renderer.
# No separate Core web service is started on the board.
for path in (repository / "src/k1link").rglob("*"):
if path.is_file() and path.suffix in (".py", ".json"):
files.append(("usr/lib/mission-core-node/k1/src/k1link/" + str(path.relative_to(repository / "src/k1link")), path.read_bytes(), 0o644))
for relative in ("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"):
files.append(("usr/lib/mission-core-node/k1/" + relative, (repository / relative).read_bytes(), 0o644))
sdk = ROOT.parents[1] / "packages/plugin-sdk/python/missioncore_plugin_sdk"
for path in sdk.rglob("*.py"):
files.append(("usr/lib/mission-core-node/sdk/missioncore_plugin_sdk/" + str(path.relative_to(sdk)), path.read_bytes(), 0o644))
@@ -1,13 +1,18 @@
"""Engineering build input, never run on an operator board. Exact PyPI hashes only."""
import hashlib
import argparse
import json
import time
from pathlib import Path
from urllib.request import urlopen
from urllib.request import Request, urlopen
root = Path(__file__).resolve().parents[1]
manifest = json.loads((root / "packaging/realsense-bundle.json").read_text())
output = root / "build/realsense-wheels"
parser = argparse.ArgumentParser()
parser.add_argument("--model", choices=("realsense", "k1"), default="realsense")
model = parser.parse_args().model
manifest = json.loads((root / f"packaging/{model}-bundle.json").read_text())
output = root / f"build/{model}-wheels"
output.mkdir(parents=True, exist_ok=True)
for item in manifest["wheels"]:
target = output / item["name"]
@@ -23,8 +28,24 @@ for item in manifest["wheels"]:
)
if not source["url"].startswith("https://files.pythonhosted.org/"):
raise ValueError("Unexpected package origin")
with urlopen(source["url"], timeout=120) as response:
data = response.read(item["bytes"] + 1)
partial = target.with_suffix(target.suffix + ".partial")
for attempt in range(4):
offset = partial.stat().st_size if partial.exists() else 0
request = Request(source["url"], headers={"Range": f"bytes={offset}-"} if offset else {})
try:
with urlopen(request, timeout=60) as response:
if offset and response.status != 206:
raise RuntimeError("Package server did not honor resume range")
with partial.open("ab" if offset else "wb") as stream:
while chunk := response.read(1024 * 1024):
stream.write(chunk)
break
except (OSError, TimeoutError):
if attempt == 3:
raise
time.sleep(2)
data = partial.read_bytes()
if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("Driver checksum mismatch")
target.write_bytes(data)
partial.replace(target)
print(json.dumps({"model": model, "wheel": item["name"], "bytes": len(data)}), flush=True)
@@ -0,0 +1,36 @@
#!/usr/bin/python3 -I
"""Administrator-only import of the exact application key from protected stdin."""
import os
import subprocess
import sys
import tempfile
from pathlib import Path
if os.geteuid() != 0 or len(sys.argv) != 1:
raise SystemExit("Root stdin import required")
secret = bytearray(sys.stdin.buffer.read(1025).strip())
try:
if len(secret) != 36 or any(v < 33 or v > 126 for v in secret):
raise SystemExit("Credential does not match the reviewed K1 profile")
root = Path("/etc/credstore.encrypted")
root.mkdir(mode=0o700, exist_ok=True)
if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022:
raise SystemExit("Unsafe credential store")
path = root / "k1-application"
if path.is_symlink() or path.exists():
raise SystemExit("K1 credential already installed; explicit rotation required")
with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory:
staged = Path(directory) / "encrypted"
completed = subprocess.run(
["/usr/bin/systemd-creds", "encrypt", "--name=k1-application", "--with-key=host", "-", str(staged)],
input=secret, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30,
)
if completed.returncode:
raise SystemExit("K1 credential import failed")
staged.chmod(0o600)
with staged.open("rb") as stream:
os.fsync(stream.fileno())
# Atomic publication without overwriting a concurrently installed key.
os.link(staged, path)
finally:
secret[:] = b"\0" * len(secret)
+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
}
]
}
+26
View File
@@ -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
+6
View File
@@ -8,6 +8,10 @@ case "$1" in
if ! getent passwd mission-core-sensors >/dev/null; then
adduser --system --group --home /var/lib/mission-core-sensors --no-create-home --disabled-login mission-core-sensors
fi
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
# Only bootstrap required to open the GUI. Operational configuration is a
# versioned job started by «Настройка окружения → Сконфигурировать».
if [ -d /run/systemd/system ]; then
@@ -15,6 +19,8 @@ case "$1" in
systemctl enable mission-core-node.service
systemctl restart mission-core-node.service
systemctl try-restart mission-core-realsense.service
systemctl enable mission-core-k1.service
systemctl restart mission-core-k1.service
fi
;;
esac
+11
View File
@@ -8,6 +8,12 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then
exit 1
fi
fi
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 "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2
exit 1
fi
fi
mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true)
case "$mc_node_device_job" in
active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;;
@@ -20,6 +26,11 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then
exit 1
;;
esac
# End the admitted idle worker before dpkg replaces its Python modules.
# New UI commands now fail unavailable instead of racing the package copy.
if [ -f /usr/lib/systemd/system/mission-core-k1.service ]; then
systemctl stop mission-core-k1.service
fi
fi
. /etc/os-release
if [ "${ID:-}" != ubuntu ] || [ "${VERSION_ID:-}" != 24.04 ]; then
+8
View File
@@ -7,6 +7,12 @@ if [ -d /run/systemd/system ]; then
exit 1
fi
fi
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 "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2
exit 1
fi
fi
mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true)
case "$mc_node_device_job" in
active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;;
@@ -40,6 +46,8 @@ case "$1" in
/usr/sbin/sshd -t
systemctl try-reload-or-restart ssh.service
fi
systemctl stop mission-core-k1.service
systemctl disable mission-core-k1.service || true
systemctl stop mission-core-realsense.service
systemctl disable mission-core-realsense.service || true
systemctl stop mission-core-node.service
@@ -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()