13 KiB
apk (Alpine Package Keeper) Updates (Phase 2b)
Extends the Phase 2a pip + npm update flow to Alpine Linux system packages.
GoClaw manages system packages via a privileged pkg-helper sidecar over a
Unix socket. This document covers how apk updates are detected, applied, and
what to do when things go wrong.
See also: GitHub binary updates · pip + npm updates
1. Overview
When the gateway runs inside an Alpine-based Docker image (latest, full,
base, otel variants) in Standard edition, GET /v1/packages/updates
includes system package updates alongside GitHub binaries, pip, and npm.
Two gates must both pass for apk to appear in the availability map:
- Runtime check:
/etc/alpine-releaseis present at startup. On Debian, Ubuntu, or macOS desktop images, apk is silently omitted — no error, no update results,availability.apk = false. - Edition check:
edition.Current().SupportsApk == true. Standard edition: always true. Lite desktop (macOS/Windows): always false — system package management is not available outside containers.
Architecture note: the gateway process runs as uid 1000 (goclaw) and never
calls apk directly. All apk operations are delegated to /app/pkg-helper
(root-owned), which listens on /tmp/pkg.sock (0600, accessible only to
goclaw). This keeps the main process unprivileged.
2. Command Matrix
Commands are executed inside pkg-helper (not by the gateway directly).
| Operation | Command inside helper | Timeout |
|---|---|---|
| Refresh index | apk update |
60 s |
| List outdated | apk version -l '<' |
30 s |
| Upgrade one package | apk add -u <name> |
5 min |
| Install new (dep install) | apk add <name> |
5 min |
| Remove | apk del <name> |
5 min |
The checker runs apk update + apk version -l '<' on every Check() call.
The executor runs apk add -u <name> on POST /v1/packages/update.
3. Behavior
How the checker works
GET /v1/packages/updatestriggersApkUpdateChecker.Check().- The checker sends an
update-indexaction to pkg-helper (runsapk updateinside the container — refreshes the remote index from Alpine mirrors). - On success, it sends a
list-outdatedaction (runsapk version -l '<'). - Output is parsed line-by-line. Each line has the form:
The parser uses the rightmost
<name>-<installed_ver> < <available_ver>-<digit>boundary to split name from version, correctly handling names that contain hyphens (e.g.py3-pip,ca-certificates). - Malformed lines are skipped with a warning log; well-formed entries produce
UpdateInfostructs withSource="apk". - Results are cached with the global
UpdatesCheckTTL(default 1 hour). The cache is invalidated on successful upgrade.
Output parsing
apk version -l '<' format:
bash-5.2.21-r6 < 5.2.26-r0
py3-pip-22.0.4-r0 < 22.3-r0
ca-certificates-20230506-r0 < 20240226-r0
Name/version split uses the rightmost hyphen-digit boundary:
py3-pip-22.0.4-r0→ name=py3-pip, version=22.0.4-r0ca-certificates-20230506-r0→ name=ca-certificates, version=20230506-r0
How the executor works
POST /v1/packages/update with body {"package": "apk:<name>"}:
- HTTP handler validates the package name (strict regex — no metacharacters).
UpdateRegistry.Apply()acquires aPackageLockerlock on("apk", name).ApkUpdateExecutor.Update()sends anupgradeaction to pkg-helper.- pkg-helper acquires an in-process
sync.Mutex(serializes all apk ops). - pkg-helper runs
apk add -u <name>. On success, returns{"ok":true}. - On success, the cache entry for the package is removed; HTTP returns 200.
The per-source PackageLocker and the in-process apkMutex in pkg-helper
form a two-layer serialization guard:
PackageLocker: prevents concurrent gateway-level operations on the same(source, name)pair (e.g., dep install + update-apply racing).apkMutex: prevents concurrent apk database access from any code path inside the helper process.
pkg-helper v2 protocol
The helper uses a JSON line-oriented protocol over /tmp/pkg.sock:
Request:
{"action": "upgrade", "package": "curl"}
Success response:
{"ok": true, "data": ""}
Error response:
{"ok": false, "error": "ERROR: unable to select packages", "code": "not_found"}
New v2 fields compared to v1:
code— typed error classification (see Error Classes section)data— opaque payload forlist-outdatedresults- New actions:
upgrade,update-index,list-outdated
v1 callers that omit code on error responses receive system_error by default
in the client — backward-compat for split deployments where helper is not yet
rebuilt. However, new actions (upgrade, update-index, list-outdated) return
unknown action on a v1 helper — feature is degraded, not crashed.
4. Pre-Release Handling
Not applicable. Alpine repositories do not distinguish stable vs pre-release
in the apk version output. apk version -l '<' lists all packages where the
installed version is older than the repository version. There is no pre-release
channel concept in the Alpine package ecosystem.
The apk checker always reports available upgrades without pre-release filtering.
5. Availability — Edition × Runtime Truth Table
| Edition | Runtime | availability.apk |
apk checker registered? |
|---|---|---|---|
| Standard | Alpine (/etc/alpine-release present) |
true |
Yes |
| Standard | Debian / Ubuntu | false |
No (runtime gate) |
| Standard | macOS (dev / testing) | false |
No (runtime gate) |
| Lite (desktop) | Any | false |
No (edition gate) |
When availability.apk = false:
GET /v1/packages/updatesresponse includes"availability": {"apk": false}.- The frontend hides the apk source from the filter bar.
POST /v1/packages/updatewithapk:<name>returns 503 (source not registered) or 409 (Lite edition gate — source never wired).
The runtime check (/etc/alpine-release stat) is performed once at checker
initialization and cached. It does not re-probe on subsequent calls.
6. Error Classes
Sentinel errors are defined in internal/skills/pkg_update_helpers.go.
The code field in pkg-helper responses maps to these sentinels.
| Sentinel | code value | Trigger |
|---|---|---|
ErrInvalidApkPackageName |
validation |
Package name fails regex (metacharacter, uppercase, etc.) |
ErrUpdateApkNotFound |
not_found |
apk add -u <name> reports "unable to select" |
ErrUpdateApkConflict |
conflict or constraint |
Dependency conflict / unsatisfiable constraints |
ErrUpdateApkLocked |
locked |
/var/lib/apk/db.lock held by another process |
ErrUpdateApkNetwork |
network |
Mirror fetch timeout, DNS failure |
ErrUpdateApkPermission |
permission |
Write permission denied in /var/lib/apk |
ErrUpdateApkDiskFull |
disk_full |
No space left on /var/cache/apk or / |
ErrUpdateApkHelperUnavail |
helper_unavailable |
Socket dial failure (helper not running) |
Unclassified errors (code="" or system_error) fall back to ClassifyApkStderr
pattern matching, then to a generic wrapped error with truncated stderr (≤ 500 chars,
ANSI-stripped before logging).
HTTP status mapping (via packages_updates.go):
| Sentinel | HTTP status |
|---|---|
ErrInvalidApkPackageName |
400 Bad Request |
ErrUpdateApkNotFound |
404 Not Found |
ErrUpdateApkConflict |
409 Conflict |
ErrUpdateApkLocked |
409 Conflict |
ErrUpdateApkNetwork |
502 Bad Gateway |
ErrUpdateApkPermission |
500 Internal Server Error |
ErrUpdateApkDiskFull |
500 Internal Server Error |
ErrUpdateApkHelperUnavail |
503 Service Unavailable |
7. Runbook
"pkg-helper unavailable" (503)
/app/pkg-helper is not running, or /tmp/pkg.sock does not exist.
For Docker Alpine deployments this is an error. For bare-metal Ubuntu/Debian
deployments, /tmp/pkg.sock is expected to be absent; package install should
use the apt path instead of apk/pkg-helper.
- Check container logs:
docker logs <container> 2>&1 | grep pkg-helper - Verify the binary exists:
docker exec <container> ls -la /app/pkg-helper - If missing, the Docker image was NOT rebuilt after the pkg-helper v2 upgrade. Pull the new image and recreate the container.
- If the binary exists but the socket is missing, check that the container
entrypoint starts the helper before the gateway:
ENTRYPOINT ["/app/entrypoint.sh"].
Bare-metal Ubuntu/Debian package table
On bare-metal Ubuntu/Debian, system package install does not write the Alpine
apk-packages persist file. GoClaw records successful apt installs in
{runtimeDir}/system-packages.json and lists versions via dpkg-query.
Alias examples:
- Installing
pip3records display namepip3, apt packagepython3-pip. - Installing
github-clirecords display namegithub-cli, apt packagegh.
The System Packages table should show the display name users installed, not the underlying Debian package alias.
Bare-metal npm global prefix
On bare-metal Ubuntu/Debian, Node packages installed from the Packages page use
{runtimeDir}/npm-global as NPM_CONFIG_PREFIX. This avoids writing to
/usr/lib/node_modules, which is root-owned on standard Ubuntu installs.
Logging: the gateway emits slog.Info("package.update.apk.unavailable") when
the helper socket is unreachable. Grep for this key to confirm the symptom.
"Package database is locked" (409)
/var/lib/apk/db.lock is held by another apk process.
- Wait ~10 seconds and retry — an in-progress
apk addfrom the dep-installer may still be running (the apkMutex serializes gateway operations, but manualdocker exec apk addfrom outside the gateway bypasses it). - If the lock persists:
docker exec <container> ls -la /var/lib/apk/db.lock— if the owning PID is dead, the lock is stale. Restart the container. - Do NOT run
rm /var/lib/apk/db.lockmanually — apk may be mid-write.
Logging: slog.Warn("package.update.apk.outcome", "code", "locked").
"Disk full" (500)
/var/cache/apk or / is out of space.
- Check disk:
docker exec <container> df -h / - Clean cache:
docker exec <container> apk cache clean - Expand the container volume or prune unused images on the host.
"Dependency conflict" (409)
apk cannot resolve dependencies for the requested upgrade.
- SSH into the container:
docker exec -it <container> sh - Run manually:
apk add -u <name> --simulateto see the conflict details. - Resolution typically requires upgrading a conflicting package first, or accepting cascade upgrades. The GoClaw UI warns about cascade risk for system packages.
- If unresolvable, the package must be pinned via Dockerfile
RUN apk add.
Debugging helper protocol issues
The helper logs all actions to stderr (docker logs <container>). To trace
a specific action:
# Manual socket test (requires jq on PATH):
echo '{"action":"list-outdated","package":""}' | \
nc -U /tmp/pkg.sock | jq .
Expected response shape:
{"ok": true, "data": "bash-5.2.21-r6 < 5.2.26-r0\n"}
8. Minimum Versions
| Component | Minimum | Notes |
|---|---|---|
| Alpine Linux | 3.19 | apk version -l '<' output format stable since 3.12; 3.19 tested |
| apk-tools | 2.14 | Bundled with Alpine 3.19+; older versions may have different version -l output |
| pkg-helper | v2 (Phase 2b) | v1 helpers lack upgrade / update-index / list-outdated actions |
| Docker image | Phase 2b build | Image must be rebuilt to include the new pkg-helper binary |
9. Fixture Regeneration
Test fixtures for the apk parser live in internal/skills/testdata/. When the
Alpine version is upgraded and apk version -l '<' output format changes:
# Capture live output from a running container:
docker exec <container> apk update && \
docker exec <container> apk version -l '<' \
> internal/skills/testdata/apk_outdated_alpine319.txt
# Verify the parser handles the new format:
go test -run TestParseApkOutdated ./internal/skills/...
# Update test cases in apk_update_checker_test.go to reference the new fixture
# and expected name/version values.
Fixture files are named with the Alpine version (alpine319) so drift between
CI environments is detectable by git diff.
Updating pkg-helper v2 protocol tests
If the helper wire format changes (new fields, action names):
- Update
apk_helper_call_test.go—servePkgHelper/dialHelperhelpers. - Update
apk_update_checker_test.goandapk_update_executor_test.go— canned response maps. - Update
cmd/pkg-helper/main_test.go— v2 protocol action dispatch tests. - Run:
go test ./internal/skills/... ./cmd/pkg-helper/...to verify.