This commit is contained in:
2026-07-21 09:32:52 +07:00
parent db1ce68800
commit 0714b442be
8 changed files with 72 additions and 12 deletions

View File

@@ -2,7 +2,7 @@
FastAPI service that runs on each Linux client and listens on `127.0.0.1:5010`.
It accepts install, update, remove, task, log, installed-app, and service-control requests from `robot.installer`. It stores state in local SQLite and installs trusted `.deb` components downloaded from `robot.package`, allowlisted Ubuntu APT packages, plus Docker image components from allowed registries when Docker support is enabled.
It accepts install, update, remove, task, log, installed-app, and service-control requests from `robot.installer`. It stores state in local SQLite and installs trusted `.deb` components downloaded from `robot.package`, Ubuntu APT packages, plus Docker image components from allowed registries when Docker support is enabled.
## Development
@@ -88,7 +88,28 @@ APT components use a fixed manifest contract and never accept shell commands:
}
```
The package name must be present in `ALLOWED_APT_PACKAGES` (default: `postgresql`). After installation, the Agent discovers concrete systemd `.service` units shipped by the trusted package and reads their state with a fixed `systemctl show` argv (no shell command is accepted from the manifest or Web Client). The structured result is returned as `serviceCheckStatus` and `serviceChecks` from the task component API and is retained with the installed app for the UI.
`ALLOWED_APT_PACKAGES=*` allows every syntactically valid APT package name and is the default for newly built Agents. This lets an administrator enter an exact package code on the Web Server without updating each Agent's configuration for every new package.
To enable this behavior on an already deployed Agent:
```bash
sudo sed -i \
's/^ALLOWED_APT_PACKAGES=.*/ALLOWED_APT_PACKAGES=*/' \
/etc/local-installer-agent/agent.env
sudo systemctl restart local-installer-agent
```
To restore a restricted allowlist, use a comma-separated list without spaces:
```bash
ALLOWED_APT_PACKAGES=postgresql,nginx,redis-server,curl
```
Every allowed package must be available from the APT sources configured on the target client. The Agent runs `apt-get update`, but it does not add third-party repositories or signing keys.
APT packages can execute maintainer scripts as root during installation. Wildcard mode should therefore only be used when the Web Server and package-upload accounts are trusted and access-controlled.
After installation, the Agent discovers concrete systemd `.service` units shipped by the trusted package and reads their state with a fixed `systemctl show` argv (no shell command is accepted from the manifest or Web Client). The structured result is returned as `serviceCheckStatus` and `serviceChecks` from the task component API and is retained with the installed app for the UI.
PostgreSQL keeps an additional trusted policy: the Agent enables and starts `postgresql.service`, verifies that it is active, and waits for `pg_isready`. Other discovered package services are checked read-only; they are not blindly enabled or started because packages can ship optional or one-shot units.

View File

@@ -63,7 +63,7 @@ def _bool(name: str, default: bool) -> bool:
def get_settings() -> Settings:
robot_package_base_url = os.getenv("ROBOT_PACKAGE_BASE_URL", "https://package.pnkr.cloud").rstrip("/")
return Settings(
agent_version=os.getenv("AGENT_VERSION", "1.0.3"),
agent_version=os.getenv("AGENT_VERSION", "1.0.4"),
host=os.getenv("AGENT_HOST", "0.0.0.0"),
port=int(os.getenv("AGENT_PORT", "5010")),
robot_package_base_url=robot_package_base_url,
@@ -86,7 +86,7 @@ def get_settings() -> Settings:
),
allowed_apt_packages=_csv(
os.getenv("ALLOWED_APT_PACKAGES"),
["postgresql"],
["*"],
),
allowed_docker_registries=_csv_with_defaults(
os.getenv("ALLOWED_DOCKER_REGISTRIES"),

View File

@@ -128,8 +128,8 @@ class AptInstaller(DebInstaller):
def discover_service_units(self, package_name: str) -> list[str]:
"""Return concrete systemd service units shipped by an installed package.
The package name is already constrained by the Agent APT allowlist. It is
still passed as a single argv item and no shell expansion is performed.
The package name is already syntax-validated by the manifest validator. It
is passed as a single argv item and no shell expansion is performed.
Template units are omitted because they cannot be meaningfully checked
without an instance name.
"""

View File

@@ -18,7 +18,7 @@ class ManifestValidator:
elif component_type == "apt":
component = AptComponent.model_validate(raw_component).model_dump(by_alias=True)
allowed_packages = set(settings.allowed_apt_packages)
if component["packageName"] not in allowed_packages:
if "*" not in allowed_packages and component["packageName"] not in allowed_packages:
raise ValueError(
f"APT package is not allowed: {component['packageName']}"
)

View File

@@ -1,5 +1,5 @@
Package: local-installer-agent
Version: 1.0.3
Version: 1.0.4
Section: utils
Priority: optional
Architecture: amd64
@@ -7,4 +7,4 @@ Maintainer: Robot Team <admin@robot.package>
Depends: python3, python3-venv, python3-pip, curl
Description: Local Installer Agent for robot.installer
A local background service that installs, updates, and removes trusted .deb,
allowlisted APT, and Docker apps on the user's Linux machine.
APT repository packages, and Docker apps on the user's Linux machine.

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="${VERSION:-1.0.3}"
VERSION="${VERSION:-1.0.4}"
ARCH="${ARCH:-amd64}"
AGENT_HOST="${AGENT_HOST:-0.0.0.0}"
AGENT_PORT="${AGENT_PORT:-5010}"
ALLOWED_APT_PACKAGES="${ALLOWED_APT_PACKAGES:-*}"
DEB_COMPRESSION="${DEB_COMPRESSION:-gzip}"
PKG_NAME="local-installer-agent"
BUILD_ROOT="${BUILD_ROOT:-build}"
@@ -21,6 +22,12 @@ if [[ ! "$ARCH" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then
exit 1
fi
if [[ "$ALLOWED_APT_PACKAGES" != "*" && ! "$ALLOWED_APT_PACKAGES" =~ ^[a-zA-Z0-9._+-]+(,[a-zA-Z0-9._+-]+)*$ ]]; then
echo "Invalid ALLOWED_APT_PACKAGES: ${ALLOWED_APT_PACKAGES}" >&2
echo "Use * for any valid APT package or a comma-separated list without spaces." >&2
exit 1
fi
rm -rf "${BUILD_DIR}"
rm -f "${OUTPUT_PACKAGE}"
@@ -61,7 +68,7 @@ AGENT_PORT=${AGENT_PORT}
ROBOT_PACKAGE_BASE_URL=https://package.pnkr.cloud
ALLOWED_ORIGINS=https://app.pnkr.cloud,https://package.pnkr.cloud,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://localhost:8080,http://127.0.0.1:8080
ALLOWED_DOWNLOAD_HOSTS=package.pnkr.cloud
ALLOWED_APT_PACKAGES=postgresql
ALLOWED_APT_PACKAGES=${ALLOWED_APT_PACKAGES}
ALLOWED_DOCKER_REGISTRIES=registry.robot.package,docker.io
CACHE_DIR=/var/cache/local-installer-agent/packages
APP_DIR=/opt/robot-apps

View File

@@ -46,6 +46,21 @@ class FakeCommandRunner:
class AptManifestTests(unittest.TestCase):
def test_wildcard_accepts_any_valid_apt_package_name(self) -> None:
validator_settings = SimpleNamespace(allowed_apt_packages=["*"])
with patch("app.core.manifest_validator.settings", validator_settings):
manifest = ManifestValidator().validate(
apt_manifest(
{
"componentId": "nginx-extras",
"type": "apt",
"packageName": "nginx-extras",
}
)
)
self.assertEqual(manifest["components"][0]["packageName"], "nginx-extras")
def test_postgresql_is_accepted_when_allowlisted(self) -> None:
validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"])
with patch("app.core.manifest_validator.settings", validator_settings):
@@ -54,6 +69,23 @@ class AptManifestTests(unittest.TestCase):
self.assertEqual(manifest["components"][0]["type"], "apt")
self.assertEqual(manifest["components"][0]["packageName"], "postgresql")
def test_any_package_in_a_multi_package_allowlist_is_accepted(self) -> None:
validator_settings = SimpleNamespace(
allowed_apt_packages=["postgresql", "nginx", "redis-server"]
)
with patch("app.core.manifest_validator.settings", validator_settings):
manifest = ManifestValidator().validate(
apt_manifest(
{
"componentId": "redis-server",
"type": "apt",
"packageName": "redis-server",
}
)
)
self.assertEqual(manifest["components"][0]["packageName"], "redis-server")
def test_non_allowlisted_package_is_rejected(self) -> None:
validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"])
with patch("app.core.manifest_validator.settings", validator_settings):

View File

@@ -62,7 +62,7 @@
</div>
</div>
<div class="form-field full">
<small>APT packages only store metadata and do not need an uploaded file. Package code must be an Agent-allowlisted APT name, for example <code>postgresql</code>.</small>
<small>APT packages only store metadata and do not need an uploaded file. Enter the exact package name accepted by <code>apt-get install</code>, for example <code>postgresql</code>, <code>nginx</code>, or <code>redis-server</code>. Agents using <code>ALLOWED_APT_PACKAGES=*</code> accept any valid package code.</small>
</div>
<label class="form-field full">
<span>Docker image/tag</span>