Skip to content

Multi-host dependency support: lockfile identity, token resolution consistency, error clarity #773

@danielmeppiel

Description

@danielmeppiel

Summary

PR #587 (generic git host / Gitea support) surfaces several pre-existing architectural and DX gaps in the dependency identity, token resolution, and error-reporting paths. None of these block #587 directly, but they should land before multi-host support is GA so we don't ship known sharp edges to users adopting Gitea / Bitbucket / self-hosted GitLab.

Items

1. Lockfile identity collision: get_unique_key() omits host

DependencyReference.get_unique_key() and LockedDependency.get_unique_key() both return the bare repo_url (e.g. team/skills) without the host. Two dependencies with the same owner/repo on different hosts collide silently in apm.lock:

- name: skills
  repo_url: gitea.myorg.com/team/skills
- name: skills
  repo_url: github.com/team/skills

→ second entry overwrites the first; install order becomes load-bearing.

Severity: correctness / data-integrity. Becomes much more likely once #587 lands.

Files: src/apm_cli/models/dependency/reference.py (lines ~176-190), src/apm_cli/deps/lockfile.py (lines ~43-49, ~196).

Suggested fix: include host in get_unique_key() for non-default hosts (preserve bare owner/repo for github.com to keep existing lockfiles stable).

2. Asymmetric token resolution between download and clone paths

_download_github_file calls auth_resolver.resolve() directly (line ~1207 of github_downloader.py), while the clone path correctly routes through _resolve_dep_token() which returns None for generic hosts. The asymmetry is not a security issue (HTTPS is the transport boundary per core/auth.py:377-379), but it produces misleading 401 errors when a GitHub PAT is sent to a Gitea / GitLab server that rejects it — instead of falling through to git-credential-helper as the clone path does.

Severity: DX / error-message clarity.

Suggested fix: route _download_github_file token resolution through _resolve_dep_token() for consistency.

3. is_gitlab_hostname() is convention-based and fragile

startswith("gitlab.") misses org-managed GitLab instances on bespoke hostnames (code.acme.com, git.team.org, etc.). The whole GitLab-vs-other-generic-host bifurcation in #587 falls back to a heuristic that doesn't hold.

Severity: correctness on non-conformant hostnames.

Suggested fix: consider explicit user signaling (type: gitlab in apm.yml) or capability-detection via API probe + cache, rather than relying on hostname conventions.

4. Silent error swallowing in download fallback chain

_download_github_file runs up to 7 attempts (raw + API × ref × API version), and currently swallows non-404 errors at each step — auth failures, 5xx, network errors all surface to the user as "file not found." There's also no verbose_callback breadcrumb per attempt, so --verbose doesn't help users understand which endpoint was tried.

Severity: DX / debuggability.

Suggested fix: classify exceptions, only swallow 404, surface 401/403/5xx with the host + endpoint context. Emit one verbose_callback line per attempt.

5. Pre-existing GitHub-specific error strings reachable for generic hosts

Strings like "GitHub API rate limit exceeded" are now reachable from Gitea / GitLab error paths via the shared download code. Should be parameterized on host.

Severity: polish.

Non-goals

Not changing the deliberate project stance that "HTTPS is the transport security boundary" (auth.py:377-379) — global PATs may be sent to user-configured hosts. This issue is about correctness, identity, and error-clarity, not transport gating.

Why now

PR #587 quadruples the surface area of the download fallback chain and introduces multi-host scenarios the lockfile identity model wasn't designed for. Tracking these explicitly so they don't get forgotten when #587 lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cliCLI command surface, flags, help text (cross-cutting).area/lockfileLockfile schema, per-file provenance, integrity hashes, drift detection.priority/highShips in current or next milestonestatus/acceptedDirection approved, safe to start work.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/securitySecure by default. Content scanning, lockfile integrity, MCP trust boundaries.type/architectureDesign-impacting change (new module, pattern, contract).

    Type

    No type

    Projects

    Status

    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions