From b116243fd3777fae742338e5d539ed16d6a8baca Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 1 Jul 2026 04:07:26 +0000 Subject: [PATCH] fix: add remote tool cache gc --- scripts/src/gc-remote.ts | 58 ++++++++++++++++++++++++++++++++++++++++ scripts/src/help.ts | 1 + 2 files changed, 59 insertions(+) diff --git a/scripts/src/gc-remote.ts b/scripts/src/gc-remote.ts index 997af256..d847c5e1 100644 --- a/scripts/src/gc-remote.ts +++ b/scripts/src/gc-remote.ts @@ -13,6 +13,7 @@ interface RemoteGcOptions { buildCacheUntil: string; tmp: boolean; tmpMinAgeHours: number; + toolCaches: boolean; aptCache: boolean; coreDumps: boolean; coreDumpMinAgeHours: number; @@ -39,6 +40,7 @@ const DEFAULT_REMOTE_OPTIONS: RemoteGcOptions = { buildCacheUntil: "24h", tmp: true, tmpMinAgeHours: 24, + toolCaches: false, aptCache: true, coreDumps: true, coreDumpMinAgeHours: 1, @@ -144,6 +146,10 @@ function parseRemoteGcOptions(args: string[]): RemoteGcOptions { options.buildCache = false; } else if (arg === "--no-tmp") { options.tmp = false; + } else if (arg === "--include-tool-caches") { + options.toolCaches = true; + } else if (arg === "--no-tool-caches") { + options.toolCaches = false; } else if (arg === "--no-apt-cache") { options.aptCache = false; } else if (arg === "--no-core-dumps") { @@ -280,6 +286,24 @@ CORE_DUMP_DIR_ALLOWLIST = set([ "/root/unidesk", ]) +TOOL_CACHE_ALLOWLIST = [ + { + "id": "npm-cacache", + "path": "/root/.npm/_cacache", + "description": "Delete npm content-addressable package cache; npm can rebuild it.", + }, + { + "id": "npm-npx", + "path": "/root/.npm/_npx", + "description": "Delete npx package execution cache; npx can rebuild it.", + }, + { + "id": "bun-install-cache", + "path": "/root/.bun/install/cache", + "description": "Delete Bun install package cache; bun can rebuild it.", + }, +] + REGISTRY_REPOSITORY_ROOT = "/var/lib/hwlab/registry/docker/registry/v2/repositories" REGISTRY_ROOT = "/var/lib/hwlab/registry" REGISTRY_PROTECTED_TAGS = set([ @@ -1819,6 +1843,23 @@ def collect_candidates(observed_at): "action": {"command": ["apt-get", "clean"]}, }) + if OPTIONS.get("toolCaches", False): + for item in TOOL_CACHE_ALLOWLIST: + path = item["path"] + size = du_size(path, 8) or 0 + if size <= 0: + continue + candidates.append({ + "id": "tool-cache:%s" % item["id"], + "kind": "tool-cache-delete", + "risk": "medium", + "description": item["description"], + "path": path, + "sizeBytes": size, + "estimatedReclaimBytes": size, + "action": {"op": "rm-recursive", "allowlist": "remote-tool-cache"}, + }) + if OPTIONS.get("coreDumps", True): cutoff = time.time() - float(OPTIONS.get("coreDumpMinAgeHours") or 1) * 3600 for root in sorted(CORE_DUMP_DIR_ALLOWLIST): @@ -2037,6 +2078,14 @@ def assert_core_dump_candidate(path): if fuser["exitCode"] == 0: raise RuntimeError("refusing to remove core dump with active process reference: %s" % path) +def assert_tool_cache_candidate(path): + resolved = os.path.abspath(path) + allowed = set(item["path"] for item in TOOL_CACHE_ALLOWLIST) + if resolved not in allowed: + raise RuntimeError("refusing to remove tool cache outside allowlist: %s" % path) + if os.path.islink(resolved): + raise RuntimeError("refusing to remove symlink tool cache: %s" % path) + def execute(candidate): kind = candidate.get("kind") if kind == "journal-vacuum": @@ -2065,6 +2114,15 @@ def execute(candidate): raise RuntimeError((result["stderr"] or "apt-get clean failed").strip()) after = du_size("/var/cache/apt/archives") or 0 return {"reclaimedBytes": max(0, before - after), "commandOutput": bounded(result)} + if kind == "tool-cache-delete": + path = candidate.get("path") or "" + assert_tool_cache_candidate(path) + before = du_size(path, 8) or path_size(path) + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + elif os.path.exists(path): + os.unlink(path) + return {"reclaimedBytes": before} if kind == "tmp-path-delete": path = candidate.get("path") or "" assert_tmp_candidate(path) diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 67666fc3..3d2e0c21 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -350,6 +350,7 @@ function gcHelp(): unknown { "--result-limit N": "number of per-candidate run results returned when --full is not set; default 50", "--full|--raw": "return and run against all candidates rather than the default bounded page", "--include-browser-cache": "also remove repo-local .state/playwright-browsers cache", + "--include-tool-caches": "local and remote explicit opt-in: remove rebuildable npm/npx/Bun package caches from fixed allowlisted paths", "--include-state-artifacts": "manual local gc only: opt in to stale UniDesk .state artifact retention for allowlisted diagnostic files and deploy artifact direct directories", "--state-artifact-keep-days N": "keep recent UniDesk .state artifacts for N days; default 14; must be a positive integer", "--include-state-stale-scratch": "manual local gc only: opt in to YAML allowlisted stale .state scratch roots; roots and keepHours come from config/unidesk-cli.yaml#gc",