Skip to content

[v2] Guarded cleanup: orphan moor-owned volumes #54

Description

@caiopizzol

Once observability lands, the next operational question is "delete what is safe to delete." This issue covers the cleanup capability, behind explicit guardrails.

Tool surface

  • moor_cleanup_plan({ scope }): dry-run only. Returns candidates by category (build_cache, dangling_image, orphan_moor_volume) with reclaimable bytes per row.
  • moor_cleanup_execute({ candidates }): stateless. Caller passes back the entries from plan. Server uses only the identifying fields (category + ID where applicable) and re-validates eligibility against current Docker state. Caller-supplied metadata such as reclaimable_bytes is ignored; the server reports the actual freed bytes after the operation.

No plan_id, no TTL, no GC. The safety is the eligibility filter, not a confirmation dance.

Plan granularity (per category)

Plan and execute shapes differ by category because the Docker API surface differs:

  • build_cache: category-level only. Docker's build cache is host-wide (one daemon-level cache), and POST /build/prune is a prune operation, not a per-cache-item delete; you cannot pre-select cache items by ID. The plan returns one row (category: "build_cache", total reclaimable from /system/df). Execute calls /build/prune as a single category action.
  • dangling_image: per-ID. The plan enumerates each dangling image with its ID and size. Execute deletes by explicit ID via DELETE /images/:id?noprune=true. The noprune=true is load-bearing: without it, Docker also removes untagged parent images of the deleted image, which would reclaim more than the plan listed and break the "delete exactly the planned IDs" contract. This is also why we deliberately avoid /images/prune.
  • orphan_moor_volume (v2): per-name. The plan enumerates each candidate by docker_name. Execute deletes by name via DELETE /volumes/:name, with eligibility re-validated for each.

v1 allowed actions

  • build_cache prune. Data-safe for running containers but can slow future builds. Label caution in plan output.
  • dangling_image deletion by ID. Label safe.
  • Nothing else. Tagged-but-unused images and any volumes are explicitly out of v1.

v2: orphan moor-owned volumes only

Eligibility (all four must hold, re-checked at execute time):

  1. Docker volume name has the moor-<project>-<volume> prefix (apps/api/volumes.ts:63).
  2. The name parses cleanly against the current Moor naming rules. Malformed moor-* names (e.g. left over from an older naming scheme or partial deletes) go in plan output as manual_review, never as auto-eligible candidates.
  3. No matching row in project_volumes.
  4. Docker reports zero container references.

Never offer non-moor volumes. Report them in plan output as manual_review and require manual cleanup.

New endpoint POST /api/server/cleanup/orphan-volumes (server-level). The project-volume DELETE route at apps/api/routes/volumes.ts:91-104 intentionally preserves Docker data after #35; piggybacking orphan cleanup on it would muddle that contract. Reuse removeVolume() from apps/api/docker.ts:481 as the underlying primitive.

Audit

Every execute writes to a cleanup_audit table: id, executed_at, candidates_json, results_json, reclaimed_bytes, error_text. MCP execute response echoes the same summary.

Notes

  • Conservative posture: when an item could plausibly be in use, exclude it from plan rather than relying on the caller to deselect.
  • Confirmation tokens are intentionally not used. The eligibility filter is the actual safety mechanism.
  • Re-validation at execute time is mandatory because Docker state can change between plan and execute (a container could pull a previously dangling image, a volume could get attached). Skipping it makes the whole tool unsafe.

Sources: Docker Engine API /build/prune, DELETE /images/{name} (including the noprune query param), DELETE /volumes/{name}.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions