Skip to content

bug: apm install plugin@marketplace validation bypasses registry proxy #615

@chkp-roniz

Description

@chkp-roniz

Bug

When PROXY_REGISTRY_ONLY=1 and PROXY_REGISTRY_URL are set, apm install plugin@marketplace still hits the GitHub API directly during the package validation phase. This breaks the air-gapped guarantee.

Reproduction

export PROXY_REGISTRY_URL='https://art.example.com/artifactory/github'
export PROXY_REGISTRY_ONLY=1

apm marketplace add anthropics/skills    # works (after #506)
apm install claude-api@skills --verbose

Output shows direct GitHub API call during validation:

Resolving claude-api@skills via marketplace...
Resolved to: anthropics/skills
Auth resolved: host=github.com, org=anthropics, source=git-credential-fill, type=oauth
Trying unauthenticated access to github.com
API https://api.github.com/repos/anthropics/skills -> 200   <-- bypasses proxy

In a truly air-gapped environment (no GitHub access), validation fails and blocks install even though the download itself would succeed through Artifactory.

Root Cause

_validate_package_exists() in commands/install.py (line 337) has three code paths:

  1. Local packages (line 356): checks directory exists -- no network call, fine.
  2. Virtual packages (line 375): calls validate_virtual_package_exists() which uses GitHub API -- leaks.
  3. GitHub.com packages (line 457): calls api.github.com/repos/{owner}/{repo} via AuthResolver.try_with_fallback() -- leaks.

None of these paths check RegistryConfig or route through the proxy.

Impact

With PROXY_REGISTRY_ONLY=1:

  • apm marketplace add/browse/search -- will work via proxy (after feat: Support Artifactory-hosted marketplace indexes #506)
  • apm install plugin@marketplace -- fails in air-gapped at validation step
  • apm install owner/repo -- same failure at validation step
  • apm install (from lockfile, no new packages) -- works (skips validation)

Suggested Fix

Add a proxy-aware validation path in _validate_package_exists(). When RegistryConfig.from_env() returns a config, validate through Artifactory instead of GitHub API.

Option A: Validate via Archive Entry Download (lightweight)

Use fetch_entry_from_archive() to probe for a known file (e.g., check if the archive is accessible). This reuses the existing #525 infrastructure:

def _validate_package_exists(package, verbose=False, auth_resolver=None):
    from apm_cli.models.apm_package import DependencyReference
    dep_ref = DependencyReference.parse(package)

    # ... local package check (unchanged) ...

    # Proxy-aware validation: if registry proxy is configured, validate through it
    from apm_cli.deps.registry_proxy import RegistryConfig
    cfg = RegistryConfig.from_env()
    if cfg is not None:
        return _validate_via_proxy(dep_ref, cfg, verbose_log)

    # ... existing GitHub API / git ls-remote paths (unchanged) ...

Where _validate_via_proxy():

def _validate_via_proxy(dep_ref, cfg, verbose_log=None):
    """Validate package exists by probing the registry proxy archive."""
    from apm_cli.deps.artifactory_entry import fetch_entry_from_archive
    from apm_cli.utils.github_host import build_artifactory_archive_url

    repo_parts = dep_ref.repo_url.split('/')
    if len(repo_parts) < 2:
        return False
    owner, repo = repo_parts[0], repo_parts[1]
    ref = dep_ref.reference or "main"

    # Try to fetch the archive URL (HEAD request or small file probe)
    # If Artifactory returns anything other than 404, the repo exists
    urls = build_artifactory_archive_url(cfg.host, cfg.prefix, owner, repo, ref, cfg.scheme)
    headers = cfg.get_headers() or {}
    import requests
    for url in urls:
        try:
            resp = requests.head(url, headers=headers, timeout=15, allow_redirects=True)
            if resp.status_code < 400:
                if verbose_log:
                    verbose_log(f"Proxy validation: {url} -> {resp.status_code}")
                return True
        except requests.RequestException:
            continue

    if verbose_log:
        verbose_log(f"Proxy validation: all archive URLs returned error for {owner}/{repo}")

    # When PROXY_REGISTRY_ONLY, this is definitive -- repo not available
    if cfg.enforce_only:
        return False

    # Otherwise, fall through to direct validation
    return None  # signal caller to try GitHub API

Option B: Skip validation for proxy installs

When RegistryConfig is active, trust the marketplace resolution and skip the GitHub API validation entirely. The download step will fail with a clear error if the package doesn't exist in Artifactory.

if cfg is not None:
    if verbose_log:
        verbose_log(f"Skipping GitHub API validation (registry proxy active)")
    return True  # trust marketplace resolution; download will fail if missing

Option B is simpler but less informative on failure. Option A gives better error messages.

Affected Code

  • src/apm_cli/commands/install.py lines 337-504: _validate_package_exists()
  • Specifically the GitHub API call at line 472: api_url = f"{api_base}/repos/{dep_ref.repo_url}"
  • And the virtual package validation at line 382: virtual_downloader.validate_virtual_package_exists(dep_ref)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedDeprecated: use status/accepted. Kept for issue history; will be removed in milestone 0.10.0.area/content-securityUnicode scanning, Glassworm, apm audit content checks, SARIF output.area/marketplacemarketplace.json schema, federation, authoring suite, source parity.bugDeprecated: use type/bug. Kept for issue history; will be removed in milestone 0.10.0.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/bugSomething does not work as documented.

    Type

    No type

    Projects

    Status

    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions