Package onboard K1 separately with a private portable installer
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user