ips/docs/ai/plans/2026-02-25-phase2-pkg6-client-completion.md
Till Wegmueller 9814635a32
feat: Preserve manifest text through install pipeline, add architecture plans
Manifest text is now carried through the solver's ResolvedPkg and written
directly to disk during install, eliminating the redundant re-fetch from
the repository that could silently fail. save_manifest() is now mandatory
(fatal on error) since the .p5m file on disk is the authoritative record
for pkg verify and pkg fix.

Add ADRs for libips API layer (GUI sharing), OpenID Connect auth, and
SQLite catalog as query engine (including normalized installed_actions
table). Add phase plans for code hygiene, client completion, catalog
expansion, and OIDC authentication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:28:10 +01:00

8.9 KiB

Phase 2: pkg6 Client Command Completion

Date: 2026-02-25 Status: Active Depends on: Phase 1 (architecture refactoring) Goal: Make pkg6 a usable package management client

Priority Order (by user impact)

P0 — Install Actually Works

These block everything else. Without working install, nothing downstream matters.

2.0: Manifest Text Preservation (DONE)

Problem: save_manifest() re-fetched manifest text from the repository instead of using the text already obtained during solving. If the repo was unreachable after install, the save silently failed, leaving pkg verify / pkg fix without a reference manifest.

Fix (Option B — implemented):

  • Added manifest_text: String field to ResolvedPkg in solver
  • Solver now fetches raw text via fetch_manifest_text_from_repository() and parses it, keeping both the parsed struct and original text
  • Falls back to catalog cache + JSON serialization when repo is unreachable (tests, offline)
  • save_manifest() now takes manifest_text: &str instead of re-fetching
  • Save is now mandatory (fatal error) — the .p5m file on disk is the authoritative record for pkg verify and pkg fix
  • Added Image::fetch_manifest_text_from_repository() public method

Files changed: libips/src/solver/mod.rs, libips/src/image/mod.rs, pkg6/src/main.rs

2.0b: Normalized Installed Actions Table

Problem: installed.db stores one JSON blob per package. Cross-package queries (pkg verify --all, "what owns this file?", pkg contents) require deserializing every blob — O(n * m) where n = packages and m = actions per package.

Fix: Add installed_actions table alongside the existing blob, populated during install_package(). This gives O(log n) indexed lookups for path, hash, and fmri queries.

Schema (in libips/src/repository/sqlite_catalog.rs INSTALLED_SCHEMA):

CREATE TABLE IF NOT EXISTS installed_actions (
    fmri TEXT NOT NULL,
    action_type TEXT NOT NULL,   -- file, dir, link, hardlink
    path TEXT,
    hash TEXT,
    mode TEXT,
    owner TEXT,
    grp TEXT,
    target TEXT,                  -- link target, NULL for file/dir
    FOREIGN KEY (fmri) REFERENCES installed(fmri) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_ia_path ON installed_actions(path);
CREATE INDEX IF NOT EXISTS idx_ia_hash ON installed_actions(hash);
CREATE INDEX IF NOT EXISTS idx_ia_fmri ON installed_actions(fmri);

Implementation:

  1. Add schema to INSTALLED_SCHEMA constant
  2. In InstalledPackages::add_package(), after inserting the blob, iterate manifest.files, .directories, .links and INSERT each action row
  3. Removal handled automatically via ON DELETE CASCADE from remove_package()
  4. Migration: detect missing table on open, create if absent (existing installs won't have rows until next install/rebuild)
  5. Add rebuild_installed_actions() method that re-populates from existing blobs for migration of pre-existing images

Consumers:

  • pkg verify: SELECT path, hash, mode, owner, grp FROM installed_actions WHERE fmri = ?
  • pkg contents: SELECT action_type, path, hash, target FROM installed_actions WHERE fmri = ?
  • Reverse lookup: SELECT fmri, action_type FROM installed_actions WHERE path = ?
  • pkg uninstall: SELECT path, action_type FROM installed_actions WHERE fmri = ? ORDER BY path DESC

2.1: File Payload Writing

Problem: apply_file() in actions/executors.rs creates empty files.

Fix:

  1. ActionPlan must carry a reference to the source repository
  2. During file action execution, fetch payload via repo.fetch_file(hash)
  3. Decompress (gzip/lz4) and write to target path
  4. Verify digest after write
  5. Apply mode via std::fs::set_permissions()
  6. Apply owner:group via nix::unistd::chown()

Key types to add to libips:

pub struct ActionContext {
    pub image_root: PathBuf,
    pub source_repo: Arc<dyn ReadableRepository>,
    pub dry_run: bool,
    pub progress: Option<Arc<dyn ProgressReporter>>,
}

2.2: Owner/Group Application

Problem: chown calls are TODOs.

Fix: Use nix::unistd::chown() with UID/GID lookup via nix::unistd::User::from_name() / Group::from_name(). Skip on dry_run. Warn (don't fail) if running as non-root.

2.3: Facet/Variant Filtering

Problem: All actions delivered regardless of variant.arch or facet.* tags.

Fix: Before building ActionPlan, filter manifest actions:

  • Check variant.* attributes against image variants
  • Check facet.* attributes against image facets
  • Only include matching actions in the plan

P1 — Uninstall and Update

2.4: Implement uninstall

  1. Parse FMRI patterns
  2. Query installed table for matching packages
  3. Check reverse dependencies (what depends on packages being removed?)
  4. Query installed_actions for file list: SELECT path, action_type FROM installed_actions WHERE fmri = ? ORDER BY path DESC
  5. Build removal ActionPlan (delete files, then dirs in reverse path order)
  6. DELETE FROM installed WHERE fmri = ?installed_actions rows cleaned via CASCADE
  7. Remove cached .p5m manifest from disk

Reverse dependency query — needs new function:

/// Find all installed packages that depend on `stem`
pub fn reverse_dependencies(installed: &InstalledPackages, stem: &str) -> Result<Vec<Fmri>>

2.5: Implement update

  1. For each installed package (or specified patterns), query catalog for newer versions
  2. Run solver with installed packages as constraints + newest available
  3. Build ActionPlan with remove-old + install-new pairs
  4. Execute plan (ordered: remove files, install new files)
  5. Update installed.db

P2 — Query Commands (leverage SQLite catalog)

Wire up the FTS5 index that already exists in fts.db:

// In libips::api::search
pub fn search_packages(image: &Image, query: &str, options: &SearchOptions) -> Result<Vec<SearchResult>> {
    let fts_path = image.fts_db_path();
    let conn = Connection::open_with_flags(&fts_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
    let mut stmt = conn.prepare(
        "SELECT stem, publisher, summary FROM package_search WHERE package_search MATCH ?1"
    )?;
    // ...
}

Also wire the REST search for remote queries (server-side already fixed in previous commit).

2.7: Implement info

For installed packages: parse manifest blob from installed.db, extract metadata. For catalog packages: query package_metadata table (after ADR-003 schema expansion), fall back to manifest fetch.

Display: name, version, publisher, summary, description, category, dependencies, size, install date.

2.8: Implement contents

For installed packages: query installed_actions table (fast indexed lookup, no blob deser). For catalog packages: query file_inventory table (after Phase 3), fall back to manifest fetch.

2.9: Implement history

Add operation history table to image metadata:

CREATE TABLE IF NOT EXISTS operation_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp TEXT NOT NULL,
    operation TEXT NOT NULL,  -- install, uninstall, update
    packages TEXT NOT NULL,   -- JSON array of FMRIs
    user TEXT,
    result TEXT NOT NULL      -- success, failure
);

Record entries during install/uninstall/update. Display with pkg history.

P3 — Integrity Commands

2.10: Implement verify

For each installed package, query installed_actions (no blob deserialization needed):

  1. SELECT path, hash, mode, owner, grp, action_type FROM installed_actions WHERE fmri = ?
  2. For each file action: check exists, check size, verify SHA hash
  3. For each dir action: check exists, check mode
  4. For each link action: check exists, check target
  5. Report: OK, MISSING, CORRUPT, WRONG_PERMISSIONS
  6. Fall back to .p5m manifest text on disk (from 2.0) if installed_actions is empty (migration)
pub struct VerificationResult {
    pub fmri: Fmri,
    pub issues: Vec<VerificationIssue>,
}

pub enum VerificationIssue {
    Missing { path: PathBuf, action_type: String },
    HashMismatch { path: PathBuf, expected: String, actual: String },
    PermissionMismatch { path: PathBuf, expected: String, actual: String },
    OwnerMismatch { path: PathBuf, expected: String, actual: String },
}

2.11: Implement fix

  1. Run verify
  2. For each MISSING/CORRUPT file: re-download payload from repository
  3. For each WRONG_PERMISSIONS: re-apply mode/owner/group
  4. Report what was fixed

P4 — Remaining Action Executors

2.12: User/Group executors

Use nix crate for user/group creation. On non-illumos systems, log a warning and skip.

2.13: Driver executor

illumos-specific: call add_drv / update_drv. On other systems, skip with warning.

2.14: Service executor (SMF)

illumos-specific: call svcadm / svccfg. On other systems, skip with warning.

Verification

After each sub-step:

  • cargo nextest run passes
  • cargo clippy --workspace -- -D warnings clean
  • Manual test of the implemented command against a test repository