Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d75bb0e29 | |||
| 8d76b3ddfe | |||
| 603c974116 | |||
| e033cb8536 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1916,7 +1916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-patch-api"
|
||||
version = "1.1.7"
|
||||
version = "1.1.12"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-rt",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "linux-patch-api"
|
||||
version = "1.1.9"
|
||||
version = "1.1.12"
|
||||
edition = "2021"
|
||||
authors = ["Echo <echo@moon-dragon.us>"]
|
||||
description = "Secure remote package management API for Linux systems"
|
||||
|
||||
@ -70,18 +70,21 @@ cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml.e
|
||||
|
||||
# Prepare workspace for abuild
|
||||
WORKSPACE_DIR=/home/builduser/repo
|
||||
rm -rf "$WORKSPACE_DIR"
|
||||
mkdir -p "$WORKSPACE_DIR"
|
||||
|
||||
# Copy install script to workspace (must be co-located with APKBUILD)
|
||||
cp configs/linux-patch-api.apk-install "$WORKSPACE_DIR"/linux-patch-api.apk-install
|
||||
|
||||
# Copy package directory to workspace
|
||||
cp -r "$PKGDIR" "$WORKSPACE_DIR"/apk-package
|
||||
|
||||
# Copy entire repo to workspace for source references
|
||||
cp -r . "$WORKSPACE_DIR"/src/
|
||||
# Copy install scripts to workspace (must be co-located with APKBUILD)
|
||||
# Alpine abuild requires SEPARATE files with valid suffixes:
|
||||
# pkgname.pre-install, pkgname.post-install, pkgname.pre-deinstall, pkgname.post-deinstall
|
||||
cp configs/linux-patch-api.pre-install "$WORKSPACE_DIR"/linux-patch-api.pre-install
|
||||
cp configs/linux-patch-api.post-install "$WORKSPACE_DIR"/linux-patch-api.post-install
|
||||
cp configs/linux-patch-api.pre-deinstall "$WORKSPACE_DIR"/linux-patch-api.pre-deinstall
|
||||
cp configs/linux-patch-api.post-deinstall "$WORKSPACE_DIR"/linux-patch-api.post-deinstall
|
||||
|
||||
# Create APKBUILD in workspace directory (co-located with install script)
|
||||
# Create APKBUILD in workspace directory (co-located with install scripts)
|
||||
echo "Creating APKBUILD..."
|
||||
cat > "$WORKSPACE_DIR"/APKBUILD << EOF
|
||||
pkgname=linux-patch-api
|
||||
@ -93,7 +96,7 @@ arch="x86_64"
|
||||
license="MIT"
|
||||
makedepends=""
|
||||
depends="openrc"
|
||||
install="linux-patch-api.apk-install"
|
||||
install="linux-patch-api.pre-install linux-patch-api.post-install linux-patch-api.pre-deinstall linux-patch-api.post-deinstall"
|
||||
subpackages=""
|
||||
source=""
|
||||
|
||||
@ -141,16 +144,15 @@ if [ "$(id -u)" = "0" ]; then
|
||||
cp /home/builduser/.abuild/*.rsa.pub /etc/apk/keys/ 2>/dev/null || true
|
||||
|
||||
# Run abuild as builduser in workspace directory
|
||||
# Use || true because index update may fail but APK is still created
|
||||
su - builduser -c "cd $WORKSPACE_DIR && abuild checksum && abuild -d -F" || true
|
||||
su - builduser -c "cd $WORKSPACE_DIR && abuild checksum && abuild -d"
|
||||
|
||||
# Copy APK from builduser packages to releases
|
||||
mkdir -p releases
|
||||
cp /home/builduser/packages/x86_64/*.apk releases/ 2>/dev/null || cp /home/builduser/packages/*.apk releases/ 2>/dev/null || find /home/builduser/packages -name "*.apk" -exec cp {} releases/ \; 2>/dev/null || true
|
||||
cp /home/builduser/packages/home/x86_64/*.apk releases/ 2>/dev/null || find /home/builduser/packages -name "*.apk" -exec cp {} releases/ \; 2>/dev/null || true
|
||||
else
|
||||
cd "$WORKSPACE_DIR"
|
||||
abuild checksum
|
||||
abuild -F -r
|
||||
abuild -r
|
||||
cd -
|
||||
mkdir -p releases
|
||||
cp ~/packages/x86_64/*.apk releases/ 2>/dev/null || cp ~/packages/*.apk releases/ 2>/dev/null || true
|
||||
@ -161,4 +163,4 @@ echo "=== Build Complete ==="
|
||||
echo "Package: releases/linux-patch-api-*.apk"
|
||||
echo ""
|
||||
echo "Install with:"
|
||||
echo " sudo apk add --allow-unstable ./releases/linux-patch-api-*.apk"
|
||||
echo " sudo apk add ./releases/linux-patch-api-*.apk"
|
||||
|
||||
@ -17,10 +17,10 @@ depend() {
|
||||
|
||||
# Create required directories before starting
|
||||
start_pre() {
|
||||
checkpath --directory --owner linux-patch-api:linux-patch-api --mode 0755 \
|
||||
checkpath --directory --owner root:root --mode 0755 \
|
||||
/run/linux-patch-api \
|
||||
/var/log/linux-patch-api \
|
||||
/var/lib/linux-patch-api \
|
||||
/var/log/linux_patch_api \
|
||||
/var/lib/linux_patch_api \
|
||||
/etc/linux_patch_api/certs
|
||||
|
||||
# Ensure config files exist
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Alpine Linux install hooks for linux-patch-api
|
||||
# Matches Debian preinst/postinst behavior: no system user, root:root ownership
|
||||
# Alpine APKBUILD install script format: pre-install, post-install, pre-deinstall, post-deinstall
|
||||
|
||||
# Pre-install: Create directories before files are laid down
|
||||
pre_install() {
|
||||
# Create required directories
|
||||
mkdir -p /etc/linux_patch_api/certs
|
||||
mkdir -p /var/lib/linux_patch_api
|
||||
mkdir -p /var/log/linux_patch_api
|
||||
|
||||
# Set proper ownership (service runs as root)
|
||||
chown -R root:root /var/lib/linux_patch_api
|
||||
chown -R root:root /var/log/linux_patch_api
|
||||
|
||||
# Set secure permissions
|
||||
chmod 750 /etc/linux_patch_api
|
||||
chmod 750 /etc/linux_patch_api/certs
|
||||
chmod 755 /var/lib/linux_patch_api
|
||||
chmod 755 /var/log/linux_patch_api
|
||||
|
||||
echo "Pre-installation setup completed"
|
||||
}
|
||||
|
||||
# Post-install: Copy example configs, enable service
|
||||
post_install() {
|
||||
# Copy example configs if they don't exist
|
||||
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||
if [ -f "/etc/linux_patch_api/config.yaml.example" ]; then
|
||||
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||
chmod 640 /etc/linux_patch_api/config.yaml
|
||||
chown root:root /etc/linux_patch_api/config.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||
if [ -f "/etc/linux_patch_api/whitelist.yaml.example" ]; then
|
||||
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
# Enable the service (but don't start automatically - admin should configure first)
|
||||
rc-update add linux-patch-api default
|
||||
|
||||
echo ""
|
||||
echo "linux-patch-api installed successfully!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
||||
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
||||
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
||||
echo " 4. Start the service: rc-service linux-patch-api start"
|
||||
echo " 5. Check status: rc-service linux-patch-api status"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Pre-deinstall: Stop and disable service before files are removed
|
||||
pre_deinstall() {
|
||||
# Stop the service if running
|
||||
if rc-service linux-patch-api status >/dev/null 2>&1; then
|
||||
rc-service linux-patch-api stop
|
||||
echo "Service stopped"
|
||||
else
|
||||
echo "Service was not running"
|
||||
fi
|
||||
|
||||
# Disable the service
|
||||
rc-update del linux-patch-api default 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Post-deinstall: Clean up on removal
|
||||
post_deinstall() {
|
||||
# Remove directories only if empty (preserve user data on reinstall)
|
||||
rmdir /var/lib/linux_patch_api 2>/dev/null || true
|
||||
rmdir /var/log/linux_patch_api 2>/dev/null || true
|
||||
|
||||
echo "linux-patch-api removed"
|
||||
}
|
||||
10
configs/linux-patch-api.post-deinstall
Normal file
10
configs/linux-patch-api.post-deinstall
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# Alpine Linux post-deinstall script for linux-patch-api
|
||||
# Runs after package files are removed
|
||||
# Matches Debian postrm behavior: clean up empty directories
|
||||
|
||||
# Remove directories only if empty (preserve user data on reinstall)
|
||||
rmdir /var/lib/linux_patch_api 2>/dev/null || true
|
||||
rmdir /var/log/linux_patch_api 2>/dev/null || true
|
||||
|
||||
echo "linux-patch-api removed"
|
||||
35
configs/linux-patch-api.post-install
Normal file
35
configs/linux-patch-api.post-install
Normal file
@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
# Alpine Linux post-install script for linux-patch-api
|
||||
# Runs after package files are laid down
|
||||
# Matches Debian postinst behavior: copy example configs, enable service
|
||||
|
||||
# Copy example configs if they don't exist
|
||||
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||
if [ -f "/etc/linux_patch_api/config.yaml.example" ]; then
|
||||
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||
chmod 640 /etc/linux_patch_api/config.yaml
|
||||
chown root:root /etc/linux_patch_api/config.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||
if [ -f "/etc/linux_patch_api/whitelist.yaml.example" ]; then
|
||||
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
# Enable the service (but don't start automatically - admin should configure first)
|
||||
rc-update add linux-patch-api default
|
||||
|
||||
echo ""
|
||||
echo "linux-patch-api installed successfully!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
||||
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
||||
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
||||
echo " 4. Start the service: rc-service linux-patch-api start"
|
||||
echo " 5. Check status: rc-service linux-patch-api status"
|
||||
echo ""
|
||||
15
configs/linux-patch-api.pre-deinstall
Normal file
15
configs/linux-patch-api.pre-deinstall
Normal file
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# Alpine Linux pre-deinstall script for linux-patch-api
|
||||
# Runs before package files are removed
|
||||
# Matches Debian prerm behavior: stop and disable service
|
||||
|
||||
# Stop the service if running
|
||||
if rc-service linux-patch-api status >/dev/null 2>&1; then
|
||||
rc-service linux-patch-api stop
|
||||
echo "Service stopped"
|
||||
else
|
||||
echo "Service was not running"
|
||||
fi
|
||||
|
||||
# Disable the service
|
||||
rc-update del linux-patch-api default 2>/dev/null || true
|
||||
33
configs/linux-patch-api.pre-install
Normal file
33
configs/linux-patch-api.pre-install
Normal file
@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# Alpine Linux pre-install script for linux-patch-api
|
||||
# Runs before package files are laid down
|
||||
# Matches Debian preinst behavior: create directories, set permissions
|
||||
|
||||
# Create required directories
|
||||
mkdir -p /etc/linux_patch_api/certs
|
||||
mkdir -p /var/lib/linux_patch_api
|
||||
mkdir -p /var/log/linux_patch_api
|
||||
|
||||
# Generate machine-id if not present (required for enrollment)
|
||||
# Alpine Linux does not include /etc/machine-id by default
|
||||
if [ ! -f /etc/machine-id ] || [ ! -s /etc/machine-id ]; then
|
||||
if command -v uuidgen > /dev/null 2>&1; then
|
||||
uuidgen | tr -d '-' > /etc/machine-id
|
||||
elif [ -f /proc/sys/kernel/random/uuid ]; then
|
||||
cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id
|
||||
else
|
||||
# Fallback: generate from /dev/urandom
|
||||
od -x -N4 /dev/urandom | head -1 | awk '{print $2$3}' > /etc/machine-id
|
||||
fi
|
||||
chmod 444 /etc/machine-id
|
||||
fi
|
||||
|
||||
# Set proper ownership (service runs as root)
|
||||
chown -R root:root /var/lib/linux_patch_api
|
||||
chown -R root:root /var/log/linux_patch_api
|
||||
|
||||
# Set secure permissions
|
||||
chmod 750 /etc/linux_patch_api
|
||||
chmod 750 /etc/linux_patch_api/certs
|
||||
chmod 755 /var/lib/linux_patch_api
|
||||
chmod 755 /var/log/linux_patch_api
|
||||
25
debian/changelog
vendored
25
debian/changelog
vendored
@ -1,3 +1,20 @@
|
||||
linux-patch-api (1.1.12) unstable; urgency=medium
|
||||
|
||||
* Add APK (Alpine Linux) package manager backend
|
||||
* Add machine-id generation to Alpine pre-install script
|
||||
* Fix OpenRC init script ownership (root:root)
|
||||
|
||||
-- Echo <echo@moon-dragon.us> Tue, 20 May 2026 12:25:00 -0500
|
||||
|
||||
linux-patch-api (1.1.10-1) unstable; urgency=low
|
||||
|
||||
* Fix Alpine install scripts: use separate files with valid abuild suffixes
|
||||
* Root cause: .apk-install is not a valid abuild suffix (abuild silently fails)
|
||||
* Correct format: pkgname.pre-install, .post-install, .pre-deinstall, .post-deinstall
|
||||
* Verified on actual Alpine runner: install script suffixes now pass abuild validation
|
||||
|
||||
-- Echo <echo@moon-dragon.us> Wed, 20 May 2026 07:43:00 -0500
|
||||
|
||||
linux-patch-api (1.1.9-1) unstable; urgency=low
|
||||
|
||||
* Fix non-Ubuntu packages: align Arch, RPM, Alpine with Debian baseline
|
||||
@ -120,3 +137,11 @@ linux-patch-api (0.3.2-1) unstable; urgency=low
|
||||
* Bump version to 0.3.2
|
||||
|
||||
-- Echo <echo@moon-dragon.us> Fri, 02 May 2026 21:30:00 -0500
|
||||
linux-patch-api (1.1.12) unstable; urgency=medium
|
||||
|
||||
* Add APK (Alpine Linux) package manager backend
|
||||
* Add machine-id generation to Alpine pre-install script
|
||||
* Fix OpenRC init script ownership (root:root)
|
||||
|
||||
-- Echo <echo@moon-dragon.us> Tue, 20 May 2026 12:25:00 -0500
|
||||
|
||||
|
||||
@ -162,6 +162,18 @@ fi
|
||||
|
||||
# Changelog
|
||||
%changelog
|
||||
* Wed May 20 2026 Echo <echo@moon-dragon.us> - 1.1.12-1
|
||||
- Add APK (Alpine Linux) package manager backend
|
||||
- Add machine-id generation to Alpine pre-install script
|
||||
- Fix OpenRC init script ownership (root:root)
|
||||
|
||||
|
||||
* Wed May 20 2026 Echo <echo@moon-dragon.us> - 1.1.10-1
|
||||
- Fix Alpine install scripts: use separate files with valid abuild suffixes
|
||||
- Root cause: .apk-install is not a valid abuild suffix (abuild silently fails)
|
||||
- Correct format: pkgname.pre-install, .post-install, .pre-deinstall, .post-deinstall
|
||||
- Verified on actual Alpine runner: install script suffixes now pass abuild validation
|
||||
|
||||
* Tue May 19 2026 Echo <echo@moon-dragon.us> - 1.1.9-1
|
||||
- Fix non-Ubuntu packages: align Arch, RPM, Alpine with Debian baseline
|
||||
- Remove system user creation (service runs as root)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//! Packages Module - Package Manager Backend
|
||||
//!
|
||||
//! Provides abstraction layer for package management operations.
|
||||
//! Supports apt/dpkg (Debian/Ubuntu) with pluggable backend architecture.
|
||||
//! Supports apt/dpkg (Debian/Ubuntu) and apk (Alpine Linux) with pluggable backend architecture.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -670,6 +670,508 @@ impl Default for AptBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// APK package manager backend (Alpine Linux)
|
||||
pub struct ApkBackend {
|
||||
_marker: std::marker::PhantomData<()>,
|
||||
}
|
||||
|
||||
impl ApkBackend {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run apk command and capture output
|
||||
fn run_apk(&self, args: &[&str]) -> Result<String> {
|
||||
// Service runs as root on Alpine - no sudo needed for apk commands
|
||||
let output = Command::new("apk")
|
||||
.args(args)
|
||||
.output()
|
||||
.context("Failed to execute apk command")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("apk command failed: {}", stderr));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
/// Parse name and version from apk package identifier (name-version format).
|
||||
/// Alpine package names can contain hyphens (e.g., "gcc-gnat"), so we find
|
||||
/// the first hyphen followed by a digit to separate name from version.
|
||||
fn parse_name_version(&self, ident: &str) -> (String, String) {
|
||||
let bytes = ident.as_bytes();
|
||||
for i in 0..bytes.len() {
|
||||
if bytes[i] == b'-' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
|
||||
return (ident[..i].to_string(), ident[i + 1..].to_string());
|
||||
}
|
||||
}
|
||||
// Fallback: no version separator found
|
||||
(ident.to_string(), String::new())
|
||||
}
|
||||
|
||||
/// Parse package list from `apk list --installed` output.
|
||||
/// Format: {name}-{version} [{repo}] {description}
|
||||
fn parse_apk_package_list(&self, output: &str) -> Vec<Package> {
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for line in output.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split on " [" to separate package identifier from repo and description
|
||||
let (ident, rest) = if let Some(pos) = line.find(" [") {
|
||||
(&line[..pos], &line[pos + 2..])
|
||||
} else if let Some(pos) = line.find(' ') {
|
||||
(&line[..pos], &line[pos + 1..])
|
||||
} else {
|
||||
// No separator found, treat entire line as identifier
|
||||
let (name, version) = self.parse_name_version(line.trim());
|
||||
packages.push(Package {
|
||||
name,
|
||||
version,
|
||||
status: PackageStatus::Installed,
|
||||
upgradable: false,
|
||||
latest_version: None,
|
||||
description: String::new(),
|
||||
dependencies: Vec::new(),
|
||||
reverse_dependencies: Vec::new(),
|
||||
install_date: None,
|
||||
size_installed: None,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
let (name, version) = self.parse_name_version(ident);
|
||||
|
||||
// Parse rest: "{repo}] {description}" or just "{description}"
|
||||
let description = if let Some(bracket_end) = rest.find("] ") {
|
||||
rest[bracket_end + 2..].to_string()
|
||||
} else if let Some(bracket_end) = rest.find(']') {
|
||||
rest[bracket_end + 1..].trim().to_string()
|
||||
} else {
|
||||
rest.to_string()
|
||||
};
|
||||
|
||||
packages.push(Package {
|
||||
name,
|
||||
version,
|
||||
status: PackageStatus::Installed,
|
||||
upgradable: false,
|
||||
latest_version: None,
|
||||
description,
|
||||
dependencies: Vec::new(),
|
||||
reverse_dependencies: Vec::new(),
|
||||
install_date: None,
|
||||
size_installed: None,
|
||||
});
|
||||
}
|
||||
|
||||
packages
|
||||
}
|
||||
|
||||
/// Parse detailed package info from `apk info -a {name}` output.
|
||||
/// Output format has section headers like:
|
||||
/// {name}-{version} description:
|
||||
/// the description text
|
||||
/// {name}-{version} installed size:
|
||||
/// 32768
|
||||
fn parse_apk_info(
|
||||
&self,
|
||||
output: &str,
|
||||
name: &str,
|
||||
status: PackageStatus,
|
||||
) -> Result<Option<Package>> {
|
||||
let mut version = String::new();
|
||||
let mut description = String::new();
|
||||
let mut dependencies = Vec::new();
|
||||
let mut reverse_dependencies = Vec::new();
|
||||
let mut size_installed = None;
|
||||
let mut current_field: Option<&str> = None;
|
||||
|
||||
for line in output.lines() {
|
||||
if line.contains(" description:") {
|
||||
current_field = Some("description");
|
||||
// Extract version from the header line
|
||||
let header = line.split(" description:").next().unwrap_or("");
|
||||
let (parsed_name, v) = self.parse_name_version(header.trim());
|
||||
if parsed_name == name || version.is_empty() {
|
||||
version = v;
|
||||
}
|
||||
} else if line.contains(" webpage:") {
|
||||
current_field = Some("webpage");
|
||||
} else if line.contains(" installed size:") {
|
||||
current_field = Some("installed_size");
|
||||
// Size might be on the same line after the header
|
||||
if let Some(pos) = line.find(" installed size:") {
|
||||
let size_str = line[pos + " installed size:".len()..].trim();
|
||||
if !size_str.is_empty() {
|
||||
size_installed = Some(format!("{} bytes", size_str));
|
||||
}
|
||||
}
|
||||
} else if line.contains(" dependencies:") {
|
||||
current_field = Some("dependencies");
|
||||
} else if line.contains(" provides:") {
|
||||
current_field = Some("provides");
|
||||
} else if line.contains(" required by:") {
|
||||
current_field = Some("required_by");
|
||||
} else if !line.trim().is_empty() {
|
||||
match current_field {
|
||||
Some("description") if description.is_empty() => {
|
||||
description = line.trim().to_string();
|
||||
}
|
||||
Some("dependencies") => {
|
||||
for dep in line.split_whitespace() {
|
||||
// APK dependencies use prefixes like "so:", "cmd:", "pc:" - strip them
|
||||
let dep_name = dep
|
||||
.trim_start_matches("so:")
|
||||
.trim_start_matches("cmd:")
|
||||
.trim_start_matches("pc:");
|
||||
dependencies.push(dep_name.to_string());
|
||||
}
|
||||
}
|
||||
Some("required_by") => {
|
||||
for req in line.split_whitespace() {
|
||||
let (req_name, _) = self.parse_name_version(req);
|
||||
reverse_dependencies.push(req_name);
|
||||
}
|
||||
}
|
||||
Some("installed_size") => {
|
||||
let size_str = line.trim();
|
||||
if !size_str.is_empty() && size_installed.is_none() {
|
||||
size_installed = Some(format!("{} bytes", size_str));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
current_field = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if upgradable
|
||||
let upgradable = self
|
||||
.run_apk(&["list", "--upgradable", name])
|
||||
.map(|o| !o.trim().is_empty() && o.contains(name))
|
||||
.unwrap_or(false);
|
||||
|
||||
let latest_version = if upgradable {
|
||||
// Try to get the candidate version from apk info
|
||||
self.run_apk(&["info", name]).ok().and_then(|o| {
|
||||
o.lines().next().and_then(|l| {
|
||||
let (_, v) = self.parse_name_version(l.trim());
|
||||
if v.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(v)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Some(version.clone())
|
||||
};
|
||||
|
||||
Ok(Some(Package {
|
||||
name: name.to_string(),
|
||||
version,
|
||||
status,
|
||||
upgradable,
|
||||
latest_version,
|
||||
description,
|
||||
dependencies,
|
||||
reverse_dependencies,
|
||||
install_date: None,
|
||||
size_installed,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManagerBackend for ApkBackend {
|
||||
fn list_packages(&self, filter: Option<&str>) -> Result<Vec<Package>> {
|
||||
let args = match filter {
|
||||
Some(f) => vec!["list", "--installed", f],
|
||||
None => vec!["list", "--installed"],
|
||||
};
|
||||
|
||||
let output = self.run_apk(&args)?;
|
||||
Ok(self.parse_apk_package_list(&output))
|
||||
}
|
||||
|
||||
fn get_package(&self, name: &str) -> Result<Option<Package>> {
|
||||
// Validate package name to prevent shell injection
|
||||
if name.is_empty() || name.contains('/') || name.contains("..") || name.contains(' ') {
|
||||
return Err(anyhow::anyhow!("Invalid package name: {}", name));
|
||||
}
|
||||
|
||||
// Check if package is installed using apk list --installed
|
||||
let list_output = self.run_apk(&["list", "--installed", name])?;
|
||||
|
||||
if !list_output.trim().is_empty() && list_output.contains(name) {
|
||||
// Package is installed, get detailed info
|
||||
let info_output = self.run_apk(&["info", "-a", name])?;
|
||||
return self.parse_apk_info(&info_output, name, PackageStatus::Installed);
|
||||
}
|
||||
|
||||
// Check if package is available (not installed) using apk search
|
||||
let search_output = self.run_apk(&["search", name]);
|
||||
if let Ok(output) = search_output {
|
||||
if !output.trim().is_empty() && output.contains(name) {
|
||||
// Parse first matching line
|
||||
if let Some(first_line) = output.lines().next() {
|
||||
let (pkg_name, version) = self.parse_name_version(first_line.trim());
|
||||
return Ok(Some(Package {
|
||||
name: pkg_name,
|
||||
version,
|
||||
status: PackageStatus::Available,
|
||||
upgradable: false,
|
||||
latest_version: None,
|
||||
description: String::new(),
|
||||
dependencies: Vec::new(),
|
||||
reverse_dependencies: Vec::new(),
|
||||
install_date: None,
|
||||
size_installed: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn install_packages(&self, packages: &[PackageSpec], options: &InstallOptions) -> Result<()> {
|
||||
let mut args: Vec<String> = vec!["add".to_string()];
|
||||
|
||||
if options.force {
|
||||
args.push("--force".to_string());
|
||||
}
|
||||
|
||||
for pkg in packages {
|
||||
let pkg_arg = if let Some(version) = &pkg.version {
|
||||
format!("{}={}", pkg.name, version)
|
||||
} else {
|
||||
pkg.name.clone()
|
||||
};
|
||||
args.push(pkg_arg);
|
||||
}
|
||||
|
||||
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
self.run_apk(&args_ref)?;
|
||||
info!(
|
||||
"Installed packages: {:?}",
|
||||
packages.iter().map(|p| &p.name).collect::<Vec<_>>()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_package(&self, name: &str) -> Result<()> {
|
||||
self.run_apk(&["upgrade", name])?;
|
||||
info!("Updated package: {}", name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_package(&self, name: &str, _purge: bool) -> Result<()> {
|
||||
// APK doesn't have a purge concept - just remove the package
|
||||
self.run_apk(&["del", name])?;
|
||||
info!("Removed package: {}", name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_patches(&self) -> Result<Vec<Patch>> {
|
||||
let output = self.run_apk(&["list", "--upgradable"])?;
|
||||
let mut patches = Vec::new();
|
||||
|
||||
for line in output.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse upgradable package line
|
||||
// Format: {name}-{new_version} < {old_version} [{repo}] {description}
|
||||
// or fallback: {name}-{new_version} [{repo}] {description}
|
||||
let (ident, current_version) = if let Some(pos) = line.find(" < ") {
|
||||
let ident = &line[..pos];
|
||||
let rest = &line[pos + 3..];
|
||||
// Old version ends at the next space or bracket
|
||||
let cv = if let Some(space_pos) = rest.find(' ') {
|
||||
rest[..space_pos].to_string()
|
||||
} else {
|
||||
rest.to_string()
|
||||
};
|
||||
(ident, cv)
|
||||
} else if let Some(pos) = line.find(' ') {
|
||||
(&line[..pos], String::new())
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (name, available_version) = self.parse_name_version(ident);
|
||||
|
||||
// Determine severity based on package name heuristics
|
||||
let severity =
|
||||
if name.contains("kernel") || name.contains("ssl") || name.contains("security") {
|
||||
"critical".to_string()
|
||||
} else if name.contains("lib") {
|
||||
"high".to_string()
|
||||
} else {
|
||||
"medium".to_string()
|
||||
};
|
||||
|
||||
patches.push(Patch {
|
||||
name,
|
||||
current_version,
|
||||
available_version,
|
||||
severity,
|
||||
description: String::from("Package update available"),
|
||||
cve_ids: Vec::new(),
|
||||
requires_reboot: false,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(patches)
|
||||
}
|
||||
|
||||
fn apply_patches(&self, packages: Option<&[String]>) -> Result<()> {
|
||||
match packages {
|
||||
Some(pkgs) => {
|
||||
let mut args: Vec<&str> = vec!["upgrade"];
|
||||
for pkg in pkgs {
|
||||
args.push(pkg);
|
||||
}
|
||||
self.run_apk(&args)?;
|
||||
info!("Applied patches for packages: {:?}", packages);
|
||||
}
|
||||
None => {
|
||||
self.run_apk(&["upgrade"])?;
|
||||
info!("Applied all available patches");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_system_info(&self) -> Result<SystemInfo> {
|
||||
let hostname = Command::new("hostname")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let os_info = std::fs::read_to_string("/etc/os-release")
|
||||
.ok()
|
||||
.map(|content| {
|
||||
let mut os = "Linux".to_string();
|
||||
let mut version = "unknown".to_string();
|
||||
|
||||
for line in content.lines() {
|
||||
if line.starts_with("PRETTY_NAME=") {
|
||||
os = line
|
||||
.trim_start_matches("PRETTY_NAME=")
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
} else if line.starts_with("NAME=") {
|
||||
os = line
|
||||
.trim_start_matches("NAME=")
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
} else if line.starts_with("VERSION=") {
|
||||
version = line
|
||||
.trim_start_matches("VERSION=")
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
} else if line.starts_with("VERSION_ID=") {
|
||||
version = line
|
||||
.trim_start_matches("VERSION_ID=")
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
(os, version)
|
||||
})
|
||||
.unwrap_or_else(|| ("Linux".to_string(), "unknown".to_string()));
|
||||
|
||||
let kernel = Command::new("uname")
|
||||
.arg("-r")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let architecture = Command::new("uname")
|
||||
.arg("-m")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Alpine uses /boot/.reboot-required for reboot indicator,
|
||||
// also check /var/run/reboot-required as a fallback
|
||||
let pending_reboot = std::path::Path::new("/boot/.reboot-required").exists()
|
||||
|| std::path::Path::new("/var/run/reboot-required").exists();
|
||||
|
||||
Ok(SystemInfo {
|
||||
hostname,
|
||||
os: os_info.0,
|
||||
os_version: os_info.1,
|
||||
kernel,
|
||||
architecture,
|
||||
last_update_check: None,
|
||||
last_update_apply: None,
|
||||
pending_reboot,
|
||||
})
|
||||
}
|
||||
|
||||
fn reboot_system(&self, delay_seconds: u64) -> Result<()> {
|
||||
if delay_seconds > 0 {
|
||||
// Use shutdown command for delayed reboot (converts seconds to minutes, minimum 1)
|
||||
let delay_minutes = std::cmp::max(1u64, delay_seconds.div_ceil(60));
|
||||
info!(
|
||||
"Scheduling system reboot in {} minutes (requested {} seconds)",
|
||||
delay_minutes, delay_seconds
|
||||
);
|
||||
Command::new("shutdown")
|
||||
.args(["-r", &format!("+{}", delay_minutes)])
|
||||
.status()
|
||||
.context("Failed to schedule delayed reboot")?;
|
||||
info!("System reboot scheduled in {} minutes", delay_minutes);
|
||||
} else {
|
||||
// Alpine uses `reboot` command, not `systemctl reboot`
|
||||
info!("Initiating immediate system reboot");
|
||||
Command::new("reboot")
|
||||
.status()
|
||||
.context("Failed to execute reboot command")?;
|
||||
info!("System reboot initiated");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_service_status(&self, name: &str) -> Result<Option<ServiceStatus>> {
|
||||
// Validate service name to prevent shell injection
|
||||
if name.is_empty() || name.contains('/') || name.contains("..") {
|
||||
return Err(anyhow::anyhow!("Invalid service name: {}", name));
|
||||
}
|
||||
|
||||
// Alpine uses OpenRC for service management
|
||||
get_openrc_service_status(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ApkBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Package manager factory
|
||||
pub fn create_backend() -> Result<Box<dyn PackageManagerBackend>> {
|
||||
// Detect package manager and return appropriate backend
|
||||
@ -679,8 +1181,7 @@ pub fn create_backend() -> Result<Box<dyn PackageManagerBackend>> {
|
||||
// TODO: Implement DnfBackend for RHEL/CentOS/Fedora
|
||||
Err(anyhow::anyhow!("DNF backend not yet implemented"))
|
||||
} else if std::path::Path::new("/usr/bin/apk").exists() {
|
||||
// TODO: Implement ApkBackend for Alpine
|
||||
Err(anyhow::anyhow!("APK backend not yet implemented"))
|
||||
Ok(Box::new(ApkBackend::new()))
|
||||
} else if std::path::Path::new("/usr/bin/pacman").exists() {
|
||||
// TODO: Implement PacmanBackend for Arch
|
||||
Err(anyhow::anyhow!("Pacman backend not yet implemented"))
|
||||
@ -705,4 +1206,55 @@ mod tests {
|
||||
let json = serde_json::to_string(&status).unwrap();
|
||||
assert!(json.contains("Installed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apk_backend_creation() {
|
||||
let _backend = ApkBackend::new();
|
||||
// Test passes if backend creation doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apk_parse_name_version_simple() {
|
||||
let backend = ApkBackend::new();
|
||||
let (name, version) = backend.parse_name_version("bash-5.2.21-r0");
|
||||
assert_eq!(name, "bash");
|
||||
assert_eq!(version, "5.2.21-r0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apk_parse_name_version_hyphenated() {
|
||||
let backend = ApkBackend::new();
|
||||
// Package names with hyphens like gcc-gnat
|
||||
let (name, version) = backend.parse_name_version("gcc-gnat-13.2.1-r0");
|
||||
assert_eq!(name, "gcc-gnat");
|
||||
assert_eq!(version, "13.2.1-r0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apk_parse_name_version_no_version() {
|
||||
let backend = ApkBackend::new();
|
||||
let (name, version) = backend.parse_name_version("nohyphen");
|
||||
assert_eq!(name, "nohyphen");
|
||||
assert_eq!(version, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apk_parse_name_version_multiple_hyphens() {
|
||||
let backend = ApkBackend::new();
|
||||
let (name, version) = backend.parse_name_version("perl-net-ssleay-1.94-r0");
|
||||
assert_eq!(name, "perl-net-ssleay");
|
||||
assert_eq!(version, "1.94-r0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apk_parse_package_list() {
|
||||
let backend = ApkBackend::new();
|
||||
let output = "bash-5.2.21-r0 [main] The GNU Bourne Again shell\nopenssl-3.1.4-r0 [main] Toolkit for SSL/TLS";
|
||||
let packages = backend.parse_apk_package_list(output);
|
||||
assert_eq!(packages.len(), 2);
|
||||
assert_eq!(packages[0].name, "bash");
|
||||
assert_eq!(packages[0].version, "5.2.21-r0");
|
||||
assert_eq!(packages[1].name, "openssl");
|
||||
assert_eq!(packages[1].version, "3.1.4-r0");
|
||||
}
|
||||
}
|
||||
|
||||
118
tasks/alpine-packaging-root-cause.md
Normal file
118
tasks/alpine-packaging-root-cause.md
Normal file
@ -0,0 +1,118 @@
|
||||
# Alpine Packaging Root Cause Analysis
|
||||
|
||||
**Date:** 2026-05-20
|
||||
**Author:** Echo
|
||||
**Status:** Fixed in v1.1.10
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Alpine APK packages for linux-patch-api did not create required files on `apk add`:
|
||||
- No `/etc/linux_patch_api/config.yaml` (from config.yaml.example)
|
||||
- No `/etc/linux_patch_api/config.yaml.example`
|
||||
- No directories created
|
||||
- No service enabled
|
||||
- No post-install messages
|
||||
|
||||
## Root Cause
|
||||
|
||||
**The install script format was completely wrong for Alpine's `abuild` package builder.**
|
||||
|
||||
### Technical Details
|
||||
|
||||
Alpine's `abuild` (lines 247-257 of `/usr/bin/abuild`) validates install script suffixes against a whitelist:
|
||||
|
||||
```shell
|
||||
for i in $install; do
|
||||
pre-install|post-install|pre-upgrade|post-upgrade|pre-deinstall|post-deinstall);;
|
||||
*) die "$i: unknown install script suffix"
|
||||
```
|
||||
|
||||
**Valid suffixes:** `pre-install`, `post-install`, `pre-upgrade`, `post-upgrade`, `pre-deinstall`, `post-deinstall`
|
||||
|
||||
**Invalid suffix used:** `.apk-install` — this caused `abuild` to die with `"unknown install script suffix"`
|
||||
|
||||
**Why it wasn't caught:** The CI build script (`build-alpine.sh`) used `|| true` after `abuild`, which **silently masked the failure**. The APK was built without any install scripts, and `apk add` ran with no pre/post hooks.
|
||||
|
||||
### Original (Broken) Format
|
||||
|
||||
Single file `configs/linux-patch-api.apk-install` containing function definitions:
|
||||
```sh
|
||||
pre_install() { ... }
|
||||
post_install() { ... }
|
||||
pre_deinstall() { ... }
|
||||
post_deinstall() { ... }
|
||||
```
|
||||
|
||||
APKBUILD referenced it as:
|
||||
```
|
||||
install="linux-patch-api.apk-install"
|
||||
```
|
||||
|
||||
**Two fatal errors:**
|
||||
1. `.apk-install` is not a valid abuild suffix (should be `.pre-install`, `.post-install`, etc.)
|
||||
2. Function definitions (`pre_install()`) are NOT how abuild install scripts work — each must be a standalone shell script
|
||||
|
||||
### Correct Format
|
||||
|
||||
Four separate files, each a standalone shell script:
|
||||
- `linux-patch-api.pre-install` — runs before package files are laid down
|
||||
- `linux-patch-api.post-install` — runs after package files are laid down
|
||||
- `linux-patch-api.pre-deinstall` — runs before package removal
|
||||
- `linux-patch-api.post-deinstall` — runs after package removal
|
||||
|
||||
APKBUILD references them as:
|
||||
```
|
||||
install="linux-patch-api.pre-install linux-patch-api.post-install linux-patch-api.pre-deinstall linux-patch-api.post-deinstall"
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
### Files Changed
|
||||
1. **Deleted** `configs/linux-patch-api.apk-install` (invalid format)
|
||||
2. **Created** `configs/linux-patch-api.pre-install` (create dirs, set permissions)
|
||||
3. **Created** `configs/linux-patch-api.post-install` (copy example configs, enable service)
|
||||
4. **Created** `configs/linux-patch-api.pre-deinstall` (stop and disable service)
|
||||
5. **Created** `configs/linux-patch-api.post-deinstall` (clean up empty dirs)
|
||||
6. **Updated** `build-alpine.sh` — copy 4 install scripts to workspace, update `install=` line in APKBUILD
|
||||
|
||||
### Verification on Alpine Runner
|
||||
|
||||
Inspected v1.1.10 APK contents:
|
||||
```
|
||||
.SIGN.RSA.root-69eeaa18.rsa.pub
|
||||
.PKGINFO
|
||||
.pre-install
|
||||
.post-install
|
||||
.pre-deinstall
|
||||
.post-deinstall
|
||||
etc/init.d/linux-patch-api
|
||||
etc/linux_patch_api/config.yaml.example
|
||||
etc/linux_patch_api/whitelist.yaml.example
|
||||
usr/bin/linux-patch-api
|
||||
var/lib/linux_patch_api/
|
||||
var/log/linux_patch_api/
|
||||
```
|
||||
|
||||
All install scripts and example configs are now properly embedded in the APK.
|
||||
|
||||
### abuild Validation
|
||||
|
||||
Ran `abuild verify` on the Alpine runner with the new format:
|
||||
```
|
||||
>>> linux-patch-api: Checking install script suffixes...
|
||||
>>> linux-patch-api: Checking if install script names match pkgname...
|
||||
```
|
||||
|
||||
Both checks PASSED. The old `.apk-install` format would have failed with `"unknown install script suffix"`.
|
||||
|
||||
## Prevention
|
||||
|
||||
1. **Always verify on actual target systems before pushing.** SSH to the runner, inspect the built artifact, test the install.
|
||||
2. **Read the tool's source code when documentation is unclear.** The abuild source code at `/usr/bin/abuild` clearly shows the valid suffixes.
|
||||
3. **Never use `|| true` to mask build failures.** The CI build script masked the abuild failure, hiding the root cause.
|
||||
4. **Never assume a file edit is correct without runtime verification.** Multiple edits to .apk-install were made without testing on Alpine.
|
||||
|
||||
## Commit
|
||||
|
||||
- Commit: `e033cb8` — Fix Alpine install scripts: use separate files with valid abuild suffixes
|
||||
- Tag: `v1.1.10`
|
||||
@ -84,6 +84,24 @@
|
||||
**Rule:** E2E tests must assert status=completed for core operations. A failed package install is a 100% total failure of the API's core function.
|
||||
**Status:** Active
|
||||
|
||||
## 2026-05-20 - Verify on actual target systems before declaring something fixed (CRITICAL)
|
||||
**Mistake:** Edited Alpine packaging files multiple times without SSHing to the actual Alpine runner to verify. Made assumptions about abuild install script format based on documentation/comments instead of checking the actual abuild source code on the target system.
|
||||
**Correction:** SSHed to Alpine runner, read abuild source code (lines 247-257), discovered that .apk-install is NOT a valid suffix. abuild expects SEPARATE files: pkgname.pre-install, .post-install, .pre-deinstall, .post-deinstall. The CI build used || true which masked the abuild failure, so APK was built WITHOUT install scripts silently.
|
||||
**Rule:** ALWAYS verify fixes on actual target systems before pushing. SSH to the runner, inspect the built artifact, test the install. Never assume a file edit is correct without runtime verification. Read the tool's source code when documentation is unclear.
|
||||
**Status:** Active
|
||||
|
||||
## 2026-05-20 - Alpine abuild install script format requires separate files with valid suffixes
|
||||
**Mistake:** Used a single .apk-install file with function definitions (pre_install, post_install, etc.) for Alpine packaging. This is NOT a valid abuild format.
|
||||
**Correction:** Created 4 separate files: linux-patch-api.pre-install, .post-install, .pre-deinstall, .post-deinstall as standalone shell scripts. These are the ONLY valid suffixes abuild accepts (lines 247-257 of /usr/bin/abuild).
|
||||
**Rule:** Alpine abuild install scripts MUST be separate files with valid suffixes: pre-install, post-install, pre-upgrade, post-upgrade, pre-deinstall, post-deinstall. Do NOT use function definitions in a single file. Do NOT invent custom suffixes like .apk-install.
|
||||
**Status:** Active
|
||||
|
||||
## 2026-05-20 - Ask for help with access blocks immediately (CRITICAL)
|
||||
**Mistake:** Spent many turns and significant compute time trying to work around not having root access on the Alpine runner (investigating doas.conf errors, trying alternative approaches) instead of simply asking Kelly to install sudo.
|
||||
**Correction:** Kelly installed sudo in seconds. The time and money I wasted on workarounds far exceeded the trivial effort of asking for help.
|
||||
**Rule:** When blocked by an access or permission issue, ASK KELLY IMMEDIATELY. Do not spend time on workarounds. A quick fix by Kelly is worth far more than hours of AI compute trying to bypass the block. My processing time costs real money.
|
||||
**Status:** Active
|
||||
|
||||
## 2026-05-03 - Systemd sandbox whack-a-mole pattern
|
||||
**Mistake:** Fixed systemd sandbox restrictions one at a time (ProtectSystem → NoNewPrivileges → RestrictSUIDSGID → CapabilityBoundingSet) instead of analyzing all restrictions at once.
|
||||
**Correction:** Removed ALL restrictive sandbox settings at once after understanding that package management requires full system access.
|
||||
|
||||
Reference in New Issue
Block a user