Files
goclaw/docs/packages-pip-npm.md
Duy /zuey/ 425cecb9a3 feat(packages): Phase 2b — apk update flow + pkg-helper v2 protocol (#900) (#7)
* feat(packages): add apk update flow + pkg-helper v2 protocol

- APK update checker/executor via helper IPC (runtime detection, upgrade scan via apk list --upgradable)
- BREAKING: pkg-helper v2 protocol (5 actions: check_apk/check_pip/check_npm/exec_apk/exec_pip, code/data fields, renewable 10min deadline, apkMutex, 1MB scanner)
- Edition gating: SupportsApk + IsAlpineRuntime double-gate (Standard/Full only)
- Backend 3-branch wiring: alpine/apt/yum routes + update_registry, dep_installer helpers
- i18n: 5 apk keys (EN/VI/ZH catalogs)
- Frontend: source pill Alpine badge, APK in updates-list/summary-bar/update-all modal
- E2E tests: apk_e2e build tag covering checker/executor/helper protocol
- Docs: packages-apk.md, security/changelog updates
- Plans + reports under plans/260417-1500-packages-update-phase2b-apk-pkghelper/ + plans/reports/

* docs(packages): journal Phase 2b apk + pkg-helper v2
2026-05-11 15:41:27 +07:00

197 lines
7.9 KiB
Markdown

# pip + npm Package Updates (Phase 2a)
Extends the Phase 1 GitHub binary update flow to system-wide pip and npm packages.
Closes #900 (Phase 2a).
See also: [GitHub binary updates](./packages-github.md) · [apk system package updates](./packages-apk.md)
---
## Overview
When the gateway is running in Standard edition with `pip3` and/or `npm` on PATH,
`GET /v1/packages/updates` includes pip and npm update results alongside GitHub
binaries. The UI shows a per-source pill filter; sources without a binary on PATH
are hidden automatically.
pip scope: **system-wide** (`--break-system-packages`). pip venv / user-site is not
supported in Phase 2a.
npm scope: **global** (`--global`). Per-project `node_modules` are not touched.
---
## Command Matrix
| Source | Check command | Update command | Check timeout | Update timeout |
|--------|---------------|----------------|---------------|----------------|
| pip | `pip3 list --outdated --format json --break-system-packages` | `pip3 install --upgrade --no-cache-dir --break-system-packages --upgrade-strategy only-if-needed <name>` | 30 s | 5 min |
| npm | `npm outdated --global --json` | `npm install --global <name>@<version>` | 30 s | 5 min |
Pre-release pip check appends `--pre` in a secondary call (see Pre-Release Handling).
---
## Behavior
### pip
- `pip3 list --outdated --format json` emits a JSON array; each element has
`name`, `version`, `latest_version`, `latest_filetype`.
- Exit code is always 0 whether or not updates exist.
- stderr is classified via `ClassifyPipStderr` into sentinel errors (see Error Classes).
### npm
npm's exit-code semantics are non-standard:
| Condition | Exit code | Interpretation |
|-----------|-----------|----------------|
| No outdated packages | 0 | No updates |
| Outdated packages found | 1 | Updates — parse JSON stdout |
| Real npm error (ERESOLVE, network, etc.) | 1 | stderr contains `npm ERR!` |
| Ambiguous (exit 1, no stdout, no stderr) | 1 | Treated as no-updates |
The checker inspects exit code **and** stderr for `npm ERR!` before deciding
whether exit 1 means "updates available" or "real error".
---
## Pre-Release Handling
### pip
Two-call merge strategy:
1. **Primary call** (stable only, no `--pre`): baseline list of outdated packages.
2. If any currently-installed package has a pre-release version (`IsPipPreRelease()`):
**Secondary call** with `--pre` to surface the best available upgrade target.
3. Results are merged by package name; when a name appears in both, the entry
with the lexicographically higher `latest_version` wins.
Gate: `IsPipPreRelease` matches PEP 440 patterns — `a`, `b`, `rc`, `dev`, `.pre`,
`.preview` (case-insensitive, digits optional).
### npm
Single-call strategy with a skip gate:
- If `latest` contains an npm pre-release label
(`-alpha`, `-beta`, `-rc`, `-pre`, `-preview`, `-dev`, `-nightly`, `-snapshot`)
**and** `current` does not → entry is skipped.
- If `current` is already a pre-release and `latest` is too → entry kept (user on
pre-release channel receives the newest pre-release update).
This prevents unexpected upgrades from stable channels to unstable channels.
---
## Availability Detection
A source is considered **available** when two gates both pass:
1. **Binary present**: `exec.LookPath("pip3")` / `exec.LookPath("npm")` succeeds.
2. **Edition allows it**: `edition.Current().SupportsPipNpm == true` (always true
for Standard; always false for Lite desktop).
When a source is unavailable:
- Its checker returns `UpdateCheckResult{Available: false}` (no error, no updates).
- `UpdateRegistry.Availability()` maps that source to `false`.
- `GET /v1/packages/updates` response includes `"availability": {"pip": false, "npm": false}`.
- The frontend hides that source from the filter bar.
Lite edition: `gateway_packages_wiring.go` checks `edition.Current().SupportsPipNpm`
before calling `RegisterChecker` / `RegisterExecutor`. Pip and npm checkers are
never instantiated — `registry.Sources()` returns `["github"]` only.
---
## Error Classes
Sentinel errors are defined in `internal/skills/pkg_update_helpers.go`.
### pip sentinels
| Sentinel | Trigger pattern in stderr | i18n key |
|----------|--------------------------|----------|
| `ErrUpdatePipExternallyManaged` | `externally-managed-environment` / `EXTERNALLY-MANAGED` | `packages.update.pip.externally_managed` |
| `ErrUpdatePipPermission` | `Permission denied` / `EACCES` | `packages.update.pip.permission` |
| `ErrUpdatePipNotFound` | `No matching distribution` / `Could not find a version` | `packages.update.pip.not_found` |
| `ErrUpdatePipNetwork` | `Read timed out` / `ConnectionError` / `network` | `packages.update.pip.network` |
| `ErrUpdatePipConflict` | `incompatible` / `dependency resolver` / `Shallow backtracking` | `packages.update.pip.conflict` |
### npm sentinels
| Sentinel | Trigger pattern in stderr | i18n key |
|----------|--------------------------|----------|
| `ErrUpdateNpmPermission` | `EACCES` | `packages.update.npm.permission` |
| `ErrUpdateNpmConflict` | `ERESOLVE` | `packages.update.npm.conflict` |
| `ErrUpdateNpmNetwork` | `ETIMEDOUT` / `ENOTFOUND` / `getaddrinfo` | `packages.update.npm.network` |
| `ErrUpdateNpmTargetMissing` | `ETARGET` | `packages.update.npm.target_missing` |
| `ErrUpdateNpmNotFound` | `E404` / `404` / `not in this registry` | `packages.update.npm.not_found` |
Unclassified stderr returns a generic wrapped error with a truncated reason
(≤ 500 chars, ANSI-stripped).
---
## Runbook
| Symptom | Fix |
|---------|-----|
| **pip EACCES** — gateway lacks write to site-packages | Run gateway as an owner of `/usr/lib/python3/dist-packages`, or set `PIP_TARGET=/app/data/.pip` + add it to `PYTHONPATH` |
| **npm EACCES** — global prefix owned by root | `npm config set prefix ~/.npm-global`; add `~/.npm-global/bin` to `PATH` in entrypoint |
| **npm ERESOLVE** — peer conflict blocks install | SSH into container: `npm install -g <name>@<version> --legacy-peer-deps`; re-check will clear the entry |
| **pip externally-managed (PEP 668)** | Set env var `PIP_BREAK_SYSTEM_PACKAGES=1`, or upgrade pip to ≥ 23.3 (respects the CLI flag without the env var) |
---
## Minimum Versions
| Runtime | Minimum | Recommended | Notes |
|---------|---------|-------------|-------|
| pip | 20.0 | ≥ 23.3 | `--format json` requires 20+; `--break-system-packages` without env var requires 23.3+ |
| npm | 6.0 | ≥ 10 | Older versions may not emit JSON exit 1 correctly |
| Node.js | 12 | ≥ 18 LTS | npm 10 requires Node 18+ |
---
## Shared Locker
`InstallSingleDep` (skill dep install) and `PipUpdateExecutor.Update` / `NpmUpdateExecutor.Update`
(update apply) share a single `PackageLocker` instance injected via `SetSharedPackageLocker`.
This means concurrent `pip install requests` (from a skill) and `pip upgrade requests`
(from the update flow) are serialized by the same per-key mutex. The lock key is
the bare package name (e.g. `"requests"`) scoped to the source (`"pip"` or `"npm"`).
Operators must not bypass the gateway and call `pip install` directly in parallel
with gateway operations — doing so defeats the shared lock and risks a partial-install
race.
---
## Fixture Regeneration
Test fixtures capture `pip3 list --outdated --format json` and `npm outdated -g --json`
output. When the environment's package versions change, regenerate them:
```bash
# pip fixture — include pip version in filename for drift tracking
pip3 --version # e.g., pip 24.0
pip3 list --outdated --format json --break-system-packages \
> internal/skills/testdata/pip_outdated_pip24.json
# npm fixture — include npm version in filename
npm --version # e.g., 10.5.0
npm outdated --global --json \
> internal/skills/testdata/npm_outdated_npm10.json
# Note: npm exits 1 when packages are outdated — that's expected.
# Update test cases to reference the new filename and expected values.
```
Fixture files are version-stamped in their names so drift between CI environments
is detectable by `git diff`.