Skip to content

fix(install): align validation auth chain with install#941

Merged
danielmeppiel merged 16 commits intomicrosoft:mainfrom
a1icja:fix/install-validation-auth-alignment
Apr 30, 2026
Merged

fix(install): align validation auth chain with install#941
danielmeppiel merged 16 commits intomicrosoft:mainfrom
a1icja:fix/install-validation-auth-alignment

Conversation

@a1icja
Copy link
Copy Markdown
Contributor

@a1icja a1icja commented Apr 25, 2026

fix(install): align validation auth chain with install

TL;DR

apm install owner/repo/sub#BRANCH would false-fail validation when the GitHub Contents API rejected the env-var PAT but git clone (driven by the system credential helper) would have succeeded — the same package lands fine when added to apm.yml directly because that path skips validation entirely. This PR teaches validate_virtual_package_exists two new fallbacks: a directory-exists Contents API probe, and a git ls-remote chain that mirrors _clone_with_fallback's auth attempts. Validation now agrees with install for any virtual subdirectory dep that carries an explicit #ref; path-typo rejection on the default branch is preserved.

Note

The ls-remote fallback is gated on dep_ref.reference is not None so unsuffixed owner/repo/sub (no explicit #ref) still fails fast on path typos — the gate keeps that signal.

Problem (WHY)

  • Live failure: apm install foo/bar/gee#sho --verbose errors with not accessible or doesn't exist while the equivalent entry in apm.yml installs cleanly. Verbose log shows the resolver found a per-org PAT (source=GITHUB_APM_PAT_FOO, type=classic) but validate_virtual_package_exists still returned False.
  • validate_virtual_package_exists only walks the GitHub Contents API. _clone_with_fallback walks an authenticated PAT attempt then a plain HTTPS attempt that lets the system credential helper supply the token — two stages of auth-asymmetry today.
  • [!] SSO-half-authorized PATs and EMU / fine-grained-PAT scope mismatches make this asymmetry user-visible: tokens that pass git auth but receive a 4xx on the Contents API are common in enterprise installs.

Why this matters: validation is the gate on a deterministic action, so it must agree with that action. PROSE: "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action." — when the validator's view of "accessible" diverges from git clone's view, the deterministic layer no longer dominates and the install pipeline never gets to run.

Approach (WHAT)

# Fix
1 Add a directory-exists probe to validate_virtual_package_exists: when no marker file is found, request GET /repos/{owner}/{repo}/contents/{path}?ref={ref} for the directory itself. A 200 means install will find the path.
2 Add a git ls-remote --heads --tags <url> <ref> fallback gated on dep_ref.reference is not None. Walks the same auth chain as _clone_with_fallback, so any token / credential that clones is accepted by validation.
3 Thread verbose_callback from _validate_package_exists into validate_virtual_package_exists so each probe attempt is visible under --verbose ([+] path@ref / [x] path@ref (reason)) without re-running with print statements.

Implementation (HOW)

  • src/apm_cli/deps/github_downloader.pyvalidate_virtual_package_exists rewritten with a _probe(path) helper, marker probes, and two new fallbacks. Three new private helpers: _directory_exists_at_ref, _ref_exists_via_ls_remote, _ssh_attempt_allowed. The auth chain in _ref_exists_via_ls_remote deliberately mirrors _clone_with_fallback's ordering and env builders — per-attempt env selection (self.git_env for the token attempt vs _build_noninteractive_git_env for the credential-helper attempt) is preserved so the locked-down env is only used for the token-bearing attempt.
  • src/apm_cli/install/validation.py — one-line change: pass verbose_callback=verbose_log so the new per-probe diagnostics flow through --verbose.
  • tests/test_github_downloader.py — adds TestRefExistsViaLsRemote (8 tests) pinning the auth chain: token short-circuits, 403-on-token falls back to credential helper, no-token skips first attempt, all-fail returns False, empty output is a miss not a hit, Artifactory short-circuits, SSH skipped by default, SSH added when ProtocolPreference.SSH is set.
  • CHANGELOG.md / docs/src/content/docs/getting-started/authentication.md — one Fixed entry under [Unreleased] and a paragraph describing that validation walks the same chain as install.

Diagrams

Legend: control flow inside validate_virtual_package_exists for a virtual-subdirectory dep — markers fast-path where present, the two new fallbacks reconcile validation with _clone_with_fallback. Look first at the RefGate node — that is the new gate that preserves path-typo rejection for unsuffixed deps.

flowchart TD
    Start[validate_virtual_package_exists] --> Markers{Probe markers:<br/>apm.yml, SKILL.md,<br/>plugin.json, README.md}
    Markers -->|hit| Pass[return True]
    Markers -->|all 404| DirProbe{Contents API<br/>directory-exists<br/>at ref}
    DirProbe -->|200| Pass
    DirProbe -->|404 or non-GitHub| RefGate{dep_ref.reference<br/>is not None?}
    RefGate -->|no| Fail[return False]
    RefGate -->|yes| LsRemote{git ls-remote<br/>chain}
    LsRemote -->|attempt 1: token + git_env| Pass
    LsRemote -->|attempt 2: plain HTTPS<br/>credential helper| Pass
    LsRemote -->|attempt 3: SSH<br/>only if --ssh or<br/>--allow-protocol-fallback| Pass
    LsRemote -->|all fail| Fail
Loading

Trade-offs

  • Gated ls-remote fallback on explicit #ref. Chose to require dep_ref.reference is not None; rejected unconditional fallback because path-typo rejection on the default branch is a useful fast signal. Without the gate, a typo like owner/repo/skils would pass validation just because the repo and default branch resolve.
  • Directory-exists probe still uses the Contents API. Chose to keep one cheap API call before reaching for git, rejected "go straight to ls-remote" because the Contents API path stays correct for the common case (public repos, fully-authorized PATs) and avoids spawning a git subprocess.
  • SSH attempt skipped by default. Chose to gate SSH on ProtocolPreference.SSH or --allow-protocol-fallback, rejected always-attempt because users without SSH configured would see noisy validation output and possibly hung passphrase prompts. BatchMode=yes plus ConnectTimeout=10 further protects users who DO opt in.
  • Three private helpers on GitHubPackageDownloader, not a new class. Chose to colocate the new helpers with the existing auth state; rejected a separate Validator class because the auth resolver, git env, and protocol prefs already live on the downloader and a sibling class would duplicate that state.

Benefits

  1. The reporter's failing CLI invocation (apm install foo/bar/gee#sho) now succeeds: validation defers to install for any explicit-ref virtual subdir dep.
  2. Validation/install asymmetry closed for the common SSO-half-authorized-PAT case: an env-var PAT narrower than the OS keychain credential no longer breaks apm install <pkg>.
  3. 8 new pytest tests pin the ls-remote auth chain (token short-circuit, 403 fallback, SSH gating, empty-output miss, Artifactory bypass) so a future refactor cannot silently regress lenience.
  4. --verbose now shows every probe attempt ([+] path@ref / [x] path@ref (reason)); future user reports are debuggable without re-running with print statements.
  5. No LOC-budget breach: changes land in github_downloader.py (not subject to the install-engine LOC budget) and commands/install.py is untouched.

Validation

pytest — 8 new + 24 existing validation/architecture tests:

$ .venv/bin/python -m pytest \
    tests/test_github_downloader.py::TestRefExistsViaLsRemote \
    tests/unit/test_install_command.py::TestValidationFailureReasonMessages \
    tests/unit/test_install_command.py::TestLocalPathValidationMessages \
    tests/unit/test_install_command.py::TestGenericHostSshFirstValidation \
    tests/unit/install/test_architecture_invariants.py --no-header -q
................................                                         [100%]
32 passed in 0.69s
New `TestRefExistsViaLsRemote` test list (all 8 PASSED)
test_first_attempt_with_token_succeeds_short_circuits
test_authenticated_403_falls_back_to_credential_helper
test_no_token_skips_first_attempt
test_all_attempts_fail_returns_false
test_empty_output_means_ref_not_found
test_artifactory_dep_short_circuits_without_calling_git
test_ssh_attempt_skipped_by_default
test_ssh_attempt_added_when_protocol_pref_is_ssh
Pre-existing failures observed on `main` (NOT introduced by this PR)

Reproduced on main with git stash && pytest <node-id>:

  • tests/test_github_downloader.py::TestGitHubPackageDownloader::test_resolve_git_reference_commit — sandboxed clone of fake repo.
  • tests/test_github_downloader.py::TestEnterpriseHostHandling::test_clone_fallback_respects_enterprise_host — same.
  • tests/test_github_downloader.py::TestDownloaderCredentialFallback::test_credential_fill_used_when_no_env_token — pre-existing assertion on github_token cache.
  • tests/unit/test_install_command.py::TestInstallGlobalFlag::test_global_without_packages_and_no_manifest_errors — output-string assertion that depends on terminal width.

How to test

  • On a fork or org where you have a per-org env-var PAT (e.g. GITHUB_APM_PAT_<ORG>) and a separate, more-authorized credential cached via gh auth setup-git / OS keychain, run apm install foo/bar/gee#sho --verbose against a virtual subdirectory dep on a non-default branch. Expect [+] ls-remote ok via plain HTTPS w/ credential helper (or via authenticated HTTPS) in the verbose output and successful install.
  • Run apm install <org>/<repo>/<wrong-sub-name> --verbose (no #ref) and confirm validation still fails fast with not accessible or doesn't exist — the gate on explicit-ref preserves path-typo rejection.
  • Run .venv/bin/python -m pytest tests/test_github_downloader.py::TestRefExistsViaLsRemote -v and confirm all 8 tests pass in <1s.
  • Diff vs feat/install-branch-flag: confirm this PR contains only the validation/install auth-alignment changes (no --branch flag, no new apm_cli/install/branch_flag.py, no commands/install.py edit).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

@a1icja
Copy link
Copy Markdown
Contributor Author

a1icja commented Apr 25, 2026

@microsoft-github-policy-service agree

@a1icja a1icja marked this pull request as ready for review April 25, 2026 22:39
Copilot AI review requested due to automatic review settings April 25, 2026 22:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns apm install <owner>/<repo>/<subdir>#<ref> validation behavior with the actual install/clone authentication fallbacks, reducing false-negative validation failures when the GitHub Contents API rejects a token but git clone would succeed.

Changes:

  • Extend validate_virtual_package_exists with a Contents API directory-exists probe and a gated git ls-remote auth chain fallback (mirroring _clone_with_fallback).
  • Thread verbose logging through validation so probe attempts are visible under --verbose.
  • Add targeted tests for the ls-remote fallback chain and document the updated validation/auth behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/apm_cli/deps/github_downloader.py Adds new validation probes and git ls-remote fallback helpers to reconcile validation with install auth behavior.
src/apm_cli/install/validation.py Passes through verbose_callback so the new probes emit --verbose diagnostics.
tests/test_github_downloader.py Introduces tests pinning the new ls-remote fallback chain behavior.
docs/src/content/docs/getting-started/authentication.md Documents that CLI validation now follows the same auth chain as install.
CHANGELOG.md Adds an Unreleased Fixed entry for the validation/auth alignment change.

Comment thread src/apm_cli/deps/github_downloader.py
Comment thread docs/src/content/docs/getting-started/authentication.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread src/apm_cli/deps/github_downloader.py
Copilot AI and others added 3 commits April 30, 2026 17:41
The new _ref_exists_via_ls_remote helper logged GitCommandError messages
verbatim under --verbose. For basic-auth git URLs (the default), git
surfaces the full URL including the embedded token in its error output,
so a failing authenticated attempt would leak the token into the verbose
log stream.

Route the exception message through _sanitize_git_error before logging
to keep the auth chain consistent with _clone_with_fallback (which has
sanitized for the same reason since microsoft#856).

Adds test_ls_remote_failure_log_scrubs_token_from_url pinning the guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve conflicts in CHANGELOG.md, src/apm_cli/deps/github_downloader.py,
and tests/test_github_downloader.py: keep PR's auth-chain helpers
(_directory_exists_at_ref, _ref_exists_via_ls_remote, _ssh_attempt_allowed)
and marker-paths refactor; adopt main's reformatted download_virtual_file_package
signature; move microsoft#941 entry into the existing 0.11.0 Fixed section; ruff-fix
import ordering and Optional->| None.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Five panelists filed required findings across correctness, output UX, docs completeness, and release-note quality. No single blocker dominates -- all seven required items are independently necessary before merge.

Required before merge (7 items)

  • [python-architect] ls-remote --heads --tags silently drops commit-SHA refs, so the fix does not work for owner/repo/sub#<sha> references at src/apm_cli/deps/github_downloader.py:1477

    • Why: git ls-remote --heads --tags only scans refs/heads/* and refs/tags/*. A commit SHA like abc1234f is never listed there, so _ref_exists_via_ls_remote returns False for SHA pins. The PR title claims to mirror _clone_with_fallback's auth chain, but _clone_with_fallback accepts any commit git can resolve at clone time. Users who pin a subdirectory package with an explicit commit SHA still hit the false-rejection the PR claims to fix.
    • Suggested fix: Detect hex-only ref pattern (re.fullmatch(r'[0-9a-fA-F]{7,40}', ref)) and run g.ls_remote(url, env=env) without --heads --tags before the branch/tag probe; search for the SHA in the full output.
  • [python-architect] except Exception swallows non-import errors in _ssh_attempt_allowed; should be except ImportError at src/apm_cli/deps/github_downloader.py:1498

    • Why: The try/except wraps a single relative import. Catching bare Exception means any AttributeError or NameError inside the import machinery silently returns False instead of propagating, hiding real bugs.
    • Suggested fix: except ImportError: return False
  • [cli-logging-expert] Traffic light violation: [x] used for expected-missing marker files on the normal success path at src/apm_cli/deps/github_downloader.py:1279

    • Why: The _probe helper emits [x] {path}@{ref} ({exc}) for every marker file miss. With 7 marker paths, a successful subdirectory validation prints 7 [x] lines before the final [+]. [x] means "error/must act" per APM's STATUS_SYMBOLS convention, but the code comment explicitly states marker absence is NOT a failure. Seven red signals on the clean path destroys signal-to-noise for both humans and AI agents consuming --verbose output.
    • Suggested fix: Change _probe failure log from [x] to [i] for marker-file misses. Reserve [x] for unexpected HTTP errors and genuine failures in _directory_exists_at_ref and _ref_exists_via_ls_remote.
  • [cli-logging-expert] Header line Validating virtual package... has no STATUS_SYMBOLS prefix at src/apm_cli/deps/github_downloader.py:1282

    • Why: Every other log line in this function uses a [+], [x], or [i] prefix. The preamble emits raw prose with no symbol, breaking the consistent parseable-prefix contract that --verbose consumers (including AI agents) rely on.
    • Suggested fix: _log(f' [i] Validating virtual package at ref "{ref}": {dep_ref.repo_url}/{vpath}')
  • [devx-ux-expert] --verbose probe output is not documented in cli-commands.md at docs/src/content/docs/reference/cli-commands.md:308

    • Why: The PR adds structured probe lines ([+], [x], [i]) to --verbose output during subdirectory package validation, but the Verbose mode section in cli-commands.md only describes auth source resolution. Per the DevX rule: "If a CLI change is not reflected in cli-commands.md in the same PR, that change is incomplete by definition." Users debugging SSO/EMU auth failures will not know --verbose now reveals per-probe detail.
    • Suggested fix: Extend the Verbose mode bullet to include: for subdirectory packages with an explicit ref, --verbose now shows each validation probe attempt (Contents API directory check, git ls-remote fallback) and the auth step that resolved it.
  • [devx-ux-expert] ls-remote fallback silently defers path validation in non-verbose mode, creating a confusing two-step failure for path typos at src/apm_cli/deps/github_downloader.py:1339

    • Why: When all Contents API probes miss and ls-remote resolves the ref, the code logs "deferring path validation to install" only into verbose_callback -- invisible to non-verbose users. A path typo (owner/repo/nonexistent#v1.0.0) now passes validation silently and fails later during install with a clone/copy error. The "Failure mode is the product" principle requires that every potential failure names what failed, why, and one concrete next action.
    • Suggested fix: Emit a non-verbose [!] warning (e.g., [!] path validation deferred to install (API probe inconclusive); use --verbose to see probe detail) so users know they may see a late failure if the path is a typo.
  • [oss-growth-hacker] CHANGELOG entry exposes internal implementation names instead of telling a user story at CHANGELOG.md:50

    • Why: The entry opens with _clone_with_fallback, "Contents API directory probe", and "git ls-remote fallback" -- three implementation-layer terms in the first clause. Every adjacent Fixed entry uses a bolded user-facing summary. The user story (enterprise/EMU users with SSO-constrained PATs hit a false rejection wall) is real and compelling but buried behind jargon. This is a release-note contract violation relative to the established CHANGELOG pattern, not a style preference.
    • Suggested fix: - **apm install owner/repo/sub#ref no longer false-rejects packages whose git credentials differ from your API token** -- validation now walks the same auth chain as the actual install (token, credential-helper, SSH), so EMU/SSO users with a narrower env-var PAT no longer see spurious failures the lockfile-driven install would have handled silently. (#941)

Nits (11 items, skip if you want)

  • [python-architect] from urllib.parse import quote is a stdlib import inside _directory_exists_at_ref; move to module-level imports -- no circular-dependency risk.
  • [python-architect] attempts: list = [] is missing a generic type; prefer list[tuple[str, str, dict[str, str]]] = [] so mypy can verify the loop unpacking.
  • [python-architect] git.cmd.Git() is instantiated once and reused across attempts; _clone_with_fallback creates a fresh instance per attempt. Functionally equivalent but the deviation deserves a brief comment.
  • [cli-logging-expert] ls-remote "no matching refs" uses [x] for each intermediate attempt; consider [i] for per-attempt misses and a single [x] summary only when all attempts exhaust.
  • [cli-logging-expert] [+] ref resolves via ls-remote; deferring path validation to install uses a success symbol for a weaker confirmation; consider [i] to signal this is a conditional outcome.
  • [devx-ux-expert] The new authentication.md paragraph uses internal jargon (env-var PAT, credential-helper fallback, SSO/EMU) without a pointer to the Token lookup table below.
  • [oss-growth-hacker] The new authentication.md paragraph is a 70-word run-on sentence; split at the em-dash into two sentences, or use a short intro + two-item list, for scannability.
  • [oss-growth-hacker] The docs paragraph omits the SSH fallback (attempt 3 in the implementation); SSH-only users reading the docs to understand why validation passed will be confused.
  • [supply-chain-security-expert] owner and repo are interpolated raw into the Contents API URL without URL-encoding; apply quote(owner, safe="") and quote(repo, safe="") for GHES defense-in-depth.
  • [auth-expert] github_pat_ (fine-grained PAT) prefix missing from standalone-token scrub regex in _sanitize_git_error; URL-embedded PATs are caught by the generic https://[^@]+@host`` pattern but non-URL contexts would leak.
  • [auth-expert] _directory_exists_at_ref resolves token via auth_resolver.resolve() while _ref_exists_via_ls_remote uses _resolve_dep_token() -- minor call-path inconsistency; if resolve_for_dep ever gains org-alias normalization, the two probes could silently use different tokens for the same dep.

CEO arbitration

The panel reached rare structural agreement: every required finding targets a genuine correctness or UX contract gap, and no panelist escalated a nit to required inappropriately. The Python Architect's two required findings are the load-bearing ones. The SHA-ref gap is a logical contradiction in the PR's own claim -- it says it mirrors _clone_with_fallback's auth chain, but ls-remote --heads --tags silently drops commit-SHA refs, so any owner/repo/sub#<sha> pin still false-rejects. This is a correctness regression relative to the PR's stated contract, not a nit. The bare except Exception swallow is a small but real reliability gap that must be closed before merge.

The CLI Logging Expert's two required findings are independently necessary: seven [x] lines on a clean success path violate the traffic-light contract that APM's verbose output has established, and the missing [i] prefix on the header line breaks the parseable-prefix invariant. The DevX UX Expert's two required findings are also independently necessary: the docs omission is a per-PR contract (if a CLI change is not in cli-commands.md, the change is incomplete), and the silent deferral of path validation is a real user-facing failure mode that surfaces only at install time with no attribution. The OSS Growth Hacker's CHANGELOG rewrite is required: the current entry names internal symbols where adjacent entries use bolded user-facing summaries -- this is a release-note contract violation, not a style preference.

Supply Chain Security and Auth Expert findings were all classified as nits and that classification is correct -- none rises to a blocking correctness or security gap at this scope.

Dissent resolved: No cross-panelist classification disagreements were observed. All five required-finding authors placed their findings at the correct severity level. The SHA-ref gap was filed by the Python Architect alone; no other panelist contradicted it. Siding with the Architect: the PR title's explicit "mirrors _clone_with_fallback's auth chain" claim makes this a correctness commitment the PR fails to fulfill for the SHA case, which is a common enterprise pinning pattern.

Growth/positioning note: The OSS Growth Hacker's rewrite surfaces the real positioning value hiding in this fix: enterprise and EMU users with narrower env-var PATs are the target constituency APM is actively courting, and this bug directly blocked them. The corrected CHANGELOG line -- "no longer false-rejects packages whose git credentials differ from your API token" -- is the kind of user-story framing that converts enterprise evaluators reading the release notes. If this PR ships with that line, it doubles as positioning collateral for the enterprise adoption playbook.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class GitHubPackageDownloader {
        <<IOBoundary>>
        +auth_resolver AuthResolver
        +git_env dict
        +_protocol_pref ProtocolPreference
        +_allow_fallback bool
        +validate_virtual_package_exists(dep_ref, verbose_callback) bool
        +_directory_exists_at_ref(dep_ref, path, ref, log) bool
        +_ref_exists_via_ls_remote(dep_ref, ref, log) bool
        +_ssh_attempt_allowed() bool
        +_clone_with_fallback(dep_ref, ...) Path
        +_resolve_dep_token(dep_ref) str
        +_resolve_dep_auth_ctx(dep_ref) AuthContext
        +_build_repo_url(...) str
        +_build_noninteractive_git_env(...) dict
        +_resilient_get(url, headers, timeout) Response
    }
    class AuthResolver {
        <<Strategy>>
        +resolve(host, org, port) AuthContext
        +build_error_context(...) ErrorContext
    }
    class AuthContext {
        <<ValueObject>>
        +token str
        +auth_scheme str
        +source str
        +git_env dict
    }
    class DependencyReference {
        <<ValueObject>>
        +repo_url str
        +reference str
        +virtual_path str
        +host str
        +port int
        +is_insecure bool
        +is_virtual_subdirectory() bool
        +is_virtual_collection() bool
        +is_virtual_file() bool
        +is_artifactory() bool
        +is_azure_devops() bool
    }
    class ProtocolPreference {
        <<Enum>>
        SSH
        HTTPS
        AUTO
    }
    class TransportSelector {
        <<Strategy>>
        +select(dep_ref, cli_pref, allow_fallback, has_token) TransportPlan
    }
    GitHubPackageDownloader *-- AuthResolver : resolves auth
    GitHubPackageDownloader *-- TransportSelector : selects transport
    GitHubPackageDownloader ..> AuthContext : reads
    GitHubPackageDownloader ..> DependencyReference : reads
    GitHubPackageDownloader ..> ProtocolPreference : reads
    AuthResolver ..> AuthContext : returns
    note for GitHubPackageDownloader "Chain of Responsibility (new in PR): marker-file probes -> Contents API -> git ls-remote"
    note for AuthResolver "Strategy: token -> env-var -> credential-helper"
    class GitHubPackageDownloader:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install owner/repo/sub#ref]) --> B[_validate_package_exists\nvalidation.py:189]
    B --> C[GitHubPackageDownloader\n.validate_virtual_package_exists]
    C --> D{dep_ref type?}
    D -- is_virtual_collection --> E[_probe vpath.collection.yml\ngithub_downloader.py:1282]
    D -- is_virtual_file --> F[_probe vpath\ngithub_downloader.py:1285]
    D -- is_virtual_subdirectory --> G[marker file loop\ngithub_downloader.py:1289-1302]
    G -- "apm.yml / SKILL.md /\nplugin.json variants / README.md" --> H["[NET] download_raw_file\nraw.githubusercontent.com"]
    H -- 200 --> Z([return True])
    H -- all 404 --> I["_directory_exists_at_ref\ngithub_downloader.py:1325"]
    I -- ADO or non-GitHub host --> J[skip, return False]
    I -- GitHub host --> K["[NET] _resilient_get\napi.github.com/repos/owner/repo/contents/path?ref=ref"]
    K -- HTTP 200 --> Z
    K -- HTTP 404 or error --> L{dep_ref.reference\n!= None?}
    L -- No explicit ref --> M([return False])
    L -- Explicit ref given --> N["_ref_exists_via_ls_remote\ngithub_downloader.py:1390"]
    N --> O{dep_token\nresolved?}
    O -- has token --> P["[NET][EXEC] git ls-remote --heads --tags URL ref\nAttempt 1: authenticated HTTPS\nenv=self.git_env GIT_ASKPASS=echo"]
    P -- ref found --> Z
    P -- GitCommandError/empty --> Q
    O -- no token --> Q["[NET][EXEC] git ls-remote --heads --tags URL ref\nAttempt 2: plain HTTPS + credential helper\nenv=_build_noninteractive_git_env"]
    Q -- ref found --> Z
    Q -- fails --> R{_ssh_attempt_allowed?}
    R -- No --> M
    R -- Yes --> S["[NET][EXEC] git ls-remote --heads --tags URL ref\nAttempt 3: SSH\nGIT_SSH_COMMAND=ssh -o BatchMode=yes -o ConnectTimeout=10"]
    S -- ref found --> Z
    S -- fails --> M
    E --> H
    F --> H
Loading

Design patterns

  • Used in this PR: Chain of Responsibility -- validate_virtual_package_exists applies a three-stage fallback chain (marker files -> Contents API -> git ls-remote), each stage only activating on the prior's failure, visible as the note for GitHubPackageDownloader in the class diagram above.
  • Pragmatic suggestion: none -- the current shape is the simplest correct design at this scope; adding a formal Strategy class for the probe chain would only pay off if a fourth probe type is added.

Required: 2 (ls-remote SHA gap, bare except Exception). Nits: 3.

CLI Logging Expert

Required:

  1. [x] used for expected-missing marker files -- 7 false-red signals on the clean success path violate the traffic-light rule. Demote to [i].
  2. Header line Validating virtual package... has no STATUS_SYMBOLS prefix -- breaks the parseable-prefix contract.

Nits:

  • ls-remote per-attempt misses use [x]; prefer [i] for intermediate misses.
  • [+] ref resolves via ls-remote; deferring path validation... uses a success symbol for a conditional outcome; prefer [i].

DevX UX Expert

Required:

  1. --verbose probe output not documented in cli-commands.md -- per-PR docs contract violation.
  2. ls-remote fallback silently defers path validation -- path typos pass validation and fail late at install with no attribution.

Nits:

  • authentication.md paragraph uses internal jargon without linking to the Token lookup table.
  • _log format strings mix prefix conventions inconsistently with APM's wider diagnostic output.

Supply Chain Security Expert

No findings.

Nits:

  • owner and repo raw-interpolated into Contents API URL; quote() them for GHES defense-in-depth.
  • ls-remote fallback return value is indistinguishable from full path+ref validation in audit trails; a log tag would let audit consumers distinguish.
  • Glob ref patterns in dep_ref.reference would match multiple branches -- consistent with install behavior but undocumented.

Auth Expert

No findings.

Nits:

  • github_pat_ prefix missing from standalone-token scrub regex in _sanitize_git_error.
  • _directory_exists_at_ref uses auth_resolver.resolve() while _ref_exists_via_ls_remote uses _resolve_dep_token() -- minor call-path inconsistency.

OSS Growth Hacker

Required:

  1. CHANGELOG entry exposes internal implementation names (_clone_with_fallback, "Contents API directory probe", "git ls-remote fallback") instead of a user story -- inconsistent with adjacent Fixed entries, misses the enterprise/EMU conversion opportunity.

Nits:

  • authentication.md paragraph is a 70-word run-on; split for scannability.
  • Docs paragraph omits the SSH fallback; SSH-only users reading docs will be confused.

Verdict computed deterministically: 7 required findings across 6 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #941 · ● 3.2M ·

@github-actions github-actions Bot added panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 30, 2026
@danielmeppiel
Copy link
Copy Markdown
Collaborator

Hi @a1icja -- thanks for pushing this through. The fresh state of the branch (head 0f56438b) has two stacked blockers that need to be resolved together before another panel run can land green. Sharing both up-front so you can decide the smallest path forward.

Blocker 1: file-size cap

src/apm_cli/deps/github_downloader.py is 2493 lines; the repo cap is 2400 (CI fails above it). main is 2316 -- this PR adds +177 net lines to the file, pushing it 93 lines over.

Blocker 2: APM Review Panel REJECT (7 required findings)

From the panel comment dated 2026-04-30T17:01:08Z. All seven are independently required.

  1. [python-architect] git ls-remote --heads --tags silently drops commit-SHA refs (github_downloader.py:1477). SHA pins like owner/repo/sub#abc1234 still false-reject because --heads --tags only scans refs/heads/* and refs/tags/*. Suggested fix: detect hex-only ref pattern (re.fullmatch(r'[0-9a-fA-F]{7,40}', ref)) and run g.ls_remote(url, env=env) without --heads --tags for SHAs, then scan the full output for the SHA.

  2. [python-architect] except Exception swallows non-import errors in _ssh_attempt_allowed (github_downloader.py:1498). The try/except wraps a single relative import; bare Exception hides AttributeError/NameError from import machinery. Suggested fix: except ImportError: return False.

  3. [cli-logging-expert] Traffic-light violation: [x] used for expected-missing marker files on the success path (github_downloader.py:1279). _probe emits [x] {path}@{ref} ({exc}) for every marker miss; with 7 marker paths, a successful subdirectory validation prints 7 [x] lines before the final [+]. [x] means "error/must act" per STATUS_SYMBOLS. Suggested fix: change _probe failure log from [x] to [i]. Reserve [x] for unexpected HTTP errors and genuine failures in _directory_exists_at_ref and _ref_exists_via_ls_remote.

  4. [cli-logging-expert] Header line Validating virtual package... has no STATUS_SYMBOLS prefix (github_downloader.py:1282). Breaks the parseable-prefix contract. Suggested fix: _log(f' [i] Validating virtual package at ref "{ref}": {dep_ref.repo_url}/{vpath}').

  5. [devx-ux-expert] --verbose probe output not documented in cli-commands.md (docs/src/content/docs/reference/cli-commands.md:308). Per the DevX rule, a CLI change not reflected in cli-commands.md in the same PR is incomplete. Suggested fix: extend the Verbose-mode bullet to mention that for subdirectory packages with an explicit ref, --verbose now shows each validation probe attempt (Contents API directory check, git ls-remote fallback) and the auth step that resolved it.

  6. [devx-ux-expert] ls-remote fallback silently defers path validation in non-verbose mode (github_downloader.py:1339). When all Contents-API probes miss and ls-remote resolves the ref, a path typo (owner/repo/nonexistent#v1.0.0) passes validation silently and fails later at install. Suggested fix: emit a non-verbose [!] warning, e.g. [!] path validation deferred to install (API probe inconclusive); use --verbose to see probe detail.

  7. [oss-growth-hacker] CHANGELOG entry exposes internal implementation names instead of telling a user story (CHANGELOG.md:50). Suggested fix: - **apm install owner/repo/sub#ref no longer false-rejects packages whose git credentials differ from your API token** -- validation now walks the same auth chain as the actual install (token, credential-helper, SSH), so EMU/SSO users with a narrower env-var PAT no longer see spurious failures the lockfile-driven install would have handled silently. (#941)

(Eleven nits in the panel comment are skip-if-you-want; only the seven above are required.)

How to land both blockers in one revision

The seven fixes net out to roughly +7 lines (the SHA-pin path adds ~5, the [!] warning adds ~2, the rest are neutral or trim a line). Even after the fixes, the file would still be ~2500 lines, ~100 over cap. Two clean options:

  • Option A -- trim PR scope. Drop one or two of the marker-file probes that overlap with the new directory-exists fallback, or fold _log/_probe into a single helper, until the file is back under 2400. Lowest-risk if you can identify ~100 lines of the new code that are redundant with the new fallbacks.

  • Option B -- extract a sibling module. Move _directory_exists_at_ref, _ref_exists_via_ls_remote, _ssh_attempt_allowed, and the _probe/_log helpers into a new src/apm_cli/deps/github_downloader_validation.py, called from validate_virtual_package_exists. Keeps the public class surface unchanged, removes ~150 lines from github_downloader.py, and the new module starts well under cap with room for the seven fixes.

I deliberately did not push any of this myself -- this is your PR and the structural call (trim vs split) is yours to make. Happy to open a stacked targeted PR against your branch implementing whichever option you prefer if that helps; just say the word.

…ngs + line-cap compliance

- Extract _directory_exists_at_ref, _ref_exists_via_ls_remote, _ssh_attempt_allowed,
  validate_virtual_package_exists into src/apm_cli/deps/github_downloader_validation.py
  (sibling module). github_downloader.py drops 2493 -> 2279 lines, well under
  the 2400 cap. Class keeps thin instance-method shims so existing test mocks
  (patch GitHubPackageDownloader.validate_virtual_package_exists / _ref_exists_via_ls_remote)
  continue to work unchanged.
- SHA-pin detection: re.fullmatch(r'[0-9a-fA-F]{7,40}', ref) gates ls-remote
  to omit --heads --tags for hex refs and scan the full advertised-refs
  list for a SHA-prefix match. Fixes silent false-reject of commit-SHA pins.
- Narrow except Exception -> except ImportError in _ssh_attempt_allowed so
  AttributeError/NameError from import machinery surface instead of being
  swallowed.
- Traffic-light: marker-file misses on the success path log [i] (expected),
  HTTP 404 from the directory-exists probe logs [i] (expected), and [x] is
  reserved for unexpected HTTP statuses, request exceptions, and genuine
  ls-remote failures.
- Header line gains the [i] STATUS_SYMBOLS prefix:
  '  [i] Validating virtual package at ref "...": ...'
- ls-remote fallback now emits a non-verbose [!] warning via warn_callback
  when path validation is deferred to install-time, so users in
  default-verbosity mode notice the deferral.
- CHANGELOG entry rewritten as a user story (auth-chain alignment, EMU/SSO
  PAT scenarios) instead of internal helper names.
- docs/src/content/docs/reference/cli-commands.md verbose-mode bullet now
  documents the per-probe output for subdirectory packages with explicit refs.
- See microsoft#941 panel verdict 2026-04-30T17:01:08Z

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

A PR whose title is "no longer false-rejects" must not introduce a new silent-pass and a new silent-fail in the same surface -- two independently disqualifying findings block merge, with six additional required items across logging, UX, type safety, and doc-sync contracts.

Required before merge (8 items)

  • [python-architect] Public-API type hints missing on all module-level functions at src/apm_cli/deps/github_downloader_validation.py

    • Why: validate_virtual_package_exists, _directory_exists_at_ref, _ref_exists_via_ls_remote, and _ssh_attempt_allowed are a new public extension point; parameters and return types are untyped bare Any. APM standard requires type hints on all public APIs; this is a correctness gate, not style.
    • Suggested fix: Add full type annotations matching the TYPE_CHECKING-gated GitHubPackageDownloader import; use Callable[[str], None] | None for callback params and -> bool on all four functions.
  • [python-architect] _directory_exists_at_ref unpacks repo_url.split('/', 1) without a length guard at src/apm_cli/deps/github_downloader_validation.py (~line 95)

    • Why: If dep_ref.repo_url contains no / (malformed ref), the two-target unpack raises ValueError as an unhandled exception rather than returning False. The Artifactory short-circuit pattern above it sets the precedent.
    • Suggested fix: parts = dep_ref.repo_url.split('/', 1); if len(parts) != 2: return False before unpacking.
  • [cli-logging-expert] Double [!] symbol rendered when a logger is present at src/apm_cli/install/validation.py line 193

    • Why: warn_callback is called with a message already containing the literal [!]. When logger is present, _warn calls logger.warning(msg) which prepends STATUS_SYMBOLS['warning'] == '[!]' again, producing [!] [!] path validation deferred to install ....
    • Suggested fix: Strip [!] from the hardcoded string in github_downloader_validation.py and let the logging layer apply the symbol, or call _rich_echo(msg, symbol='warning') directly without embedding the symbol in the string.
  • [devx-ux-expert] Warning message gives no recovery action when install subsequently fails at src/apm_cli/deps/github_downloader_validation.py

    • Why: [!] path validation deferred to install (API probe inconclusive); use --verbose to see probe detail names what happened but not what to do if the install then fails. Per APM's failure-mode contract, every message must name what failed, why, and one concrete next action.
    • Suggested fix: [!] path validation deferred to install (API probe inconclusive) -- if install fails, check your git credentials or run with --verbose for probe detail
  • [devx-ux-expert] packages/apm-guide skill resources not updated at packages/apm-guide/.apm/skills/apm-usage/commands.md

    • Why: cli-commands.md was updated with new --verbose probe behavior, but the shipped skill resources (commands.md and installation.md in packages/apm-guide) were not. CLI Copilot agents using the shipped skill will give stale auth/validation guidance. Rule 4 requires skill resources to ship in the same PR as any CLI behavior change they describe.
    • Suggested fix: Mirror the --verbose probe description and the credential-chain note into the apm-guide skill resources in this PR.
  • [supply-chain-security-expert] ls-remote fallback is fail-open for path existence: validation passes on ref resolution alone at src/apm_cli/deps/github_downloader_validation.py line 120

    • Why: When all marker-file probes and the Contents API directory probe fail, _ref_exists_via_ls_remote is called and if the ref resolves, the function returns True without confirming dep_ref.virtual_path exists at that ref. A user who types owner/repo/typo-subdir#v1.2 receives a passing validation gate if v1.2 exists regardless of whether typo-subdir does. The PR's own warn_callback acknowledges path validation was deferred; there is no documented guarantee the install layer catches this before writing files. APM's fail-closed requirement (Added ghe support #7) states: if a check cannot be performed, refuse, do not proceed silently.
    • Suggested fix: When ls-remote resolves the ref but path existence is unconfirmed, return False rather than True. The correct fix for SSO users is to pass the full auth chain to the Contents API probe, not to skip path validation. If deferral is truly necessary, document the weakened guarantee in enterprise/security.md and add a hard-fail at install time when the path is absent.
  • [supply-chain-security-expert] vpath passed to Contents API URL without path_security guard at src/apm_cli/deps/github_downloader_validation.py line 117

    • Why: dep_ref.virtual_path is passed directly to quote(path, safe='/'), which preserves .. segments unencoded (dots are not percent-encoded by urllib.parse.quote). A crafted virtual_path containing .. traversal components would construct a Contents API URL pointing to a different repo path than intended. Relying on GitHub's API server to normalise or 404 traversal is not APM's security contract; path_security.validate_path_segments exists precisely for this chokepoint.
    • Suggested fix: Call path_security.validate_path_segments(vpath) before constructing the Contents API URL.
  • [auth-expert] Bearer token embedded in HTTPS URL instead of injected via Authorization header for ADO packages at src/apm_cli/deps/github_downloader_validation.py

    • Why: _clone_with_fallback builds the ADO bearer URL with token=None and injects the AAD JWT via build_ado_bearer_git_env into the git env. _ref_exists_via_ls_remote passes token=dep_token to _build_repo_url with auth_scheme='bearer', embedding the raw JWT into the URL as (jwt/redacted)@host/. ADO does not accept credentials in URL-user position for bearer auth; it requires an Authorization: Bearer header. The authenticated HTTPS attempt therefore fails silently for ADO virtual-subdirectory packages resolved via bearer, undermining the auth-chain fidelity this PR sets out to achieve.
    • Suggested fix: Change token=dep_token to token=(None if dep_auth_scheme == 'bearer' else dep_token) in the _build_repo_url call for attempt 1, matching _clone_with_fallback lines 751-764.

Nits (16 items, skip if you want)

  • [python-architect] Single-expression def _log(msg): if verbose_callback: verbose_callback(msg) on one line violates PEP 8 compound statement rule.
  • [python-architect] Backward-compat shims (_directory_exists_at_ref, _ref_exists_via_ls_remote, _ssh_attempt_allowed) on GitHubPackageDownloader may be removable if no external callers patch them by name; add # TODO: remove if no external callers at minimum.
  • [python-architect] _is_sha_pin docstring is missing; the 7-character minimum floor should be explained inline.
  • [cli-logging-expert] 'use --verbose to see probe detail' in warn_callback is stale advice when the user is already running --verbose; guard the hint or rephrase as "if not already running --verbose".
  • [cli-logging-expert] Opening [i] Validating virtual package at ref ... reports intent before any probe runs; per the Newspaper test, verbose output should lead with outcome.
  • [devx-ux-expert] CHANGELOG entry leads with the internal mechanism before the user benefit; swap order for higher shareability.
  • [devx-ux-expert] authentication.md addition is dense for a first-run reader; break the single long sentence into two and add a one-line concrete example.
  • [devx-ux-expert] cli-commands.md --verbose description buries the latency implication of the ls-remote network call; a parenthetical would set expectations.
  • [supply-chain-security-expert] repo_url.split('/', 1) at line 163 raises ValueError for single-component repo_url rather than failing gracefully (overlaps python-architect required finding; treat as reinforcement).
  • [supply-chain-security-expert] SHA regex 7-char minimum (_SHA_RE) creates semantic ambiguity with hex-looking branch names like cafe123; document the ambiguity or raise the floor to 12 chars.
  • [supply-chain-security-expert] warn_callback message conflates auth-inconclusive with path-not-found, masking typosquatting signals; pass the HTTP status through so callers can emit distinct messages.
  • [oss-growth-hacker] CHANGELOG entry is written for maintainers, not users; lead with the audience: "EMU/SSO users: apm install no longer rejects packages your credential helper can fetch".
  • [oss-growth-hacker] authentication.md fix-explanation paragraph has no cross-link to troubleshooting or FAQ; add a "See also" pointer to reduce issue volume from users who land here after an auth error.
  • [auth-expert] _directory_exists_at_ref calls auth_resolver.resolve(host, owner, port=...) directly instead of resolve_for_dep(dep_ref); any future org-level logic in resolve_for_dep would be silently skipped.
  • [auth-expert] _ref_exists_via_ls_remote hand-rolls a 3-attempt list instead of delegating to TransportSelector.select(); plans can diverge for edge cases such as custom insteadOf rules.
  • [auth-expert] _sanitize_git_error defense gap: GitCommandError stores the full git command line (including the token-embedded URL); if git reformats or percent-encodes the URL the scrubber regex may miss it in the command field.

CEO arbitration

PR #941 is rejected on eight required findings distributed across five active panelists. The panel is not split on verdict; every active voice with required findings agrees the code is not shippable as written. The two heaviest findings are independently disqualifying. Supply-chain flags that the ls-remote fallback is fail-open for path existence: a user who types a misspelled subdirectory receives a passing validation gate so long as the ref resolves, and there is no documented guarantee that the install layer catches this before writing files. Auth-expert flags that bearer tokens for ADO packages are embedded in the URL rather than injected via Authorization header, mirroring exactly the class of silent failure this PR claims to fix. A PR whose title is "no longer false-rejects" must not introduce a new silent-pass and a new silent-fail in the same surface. These two findings alone constitute a security and correctness regression, not a trade-off.

The remaining six required findings -- missing type hints on the new public validation API, the unguarded repo_url.split raising ValueError, the double [!] symbol in logged warnings, the warning message giving no next action, the path-traversal risk from unvalidated virtual_path segments passed to the Contents API URL, and the stale apm-guide skill resources -- are each individually sufficient to require a revision cycle. They collectively indicate the validation module was not reviewed against APM's existing auth, logging, and security contracts before submission.

The author should address the two blocking security findings first -- replace the ls-remote True return with False when path existence is unconfirmed, and align bearer auth with the _clone_with_fallback pattern -- before the remaining findings are re-reviewed. The validation module introduced in this PR is a new public extension point; it must meet the type-hint and contract standards that apply to all other public APIs in the package.

Dissent resolved: No verdict dissent. The only overlap -- python-architect marking repo_url.split as required and supply-chain marking it as a nit -- resolves at required severity, as the ValueError is an unhandled exception on a public code path. The warning message findings from cli-logging-expert (double symbol) and devx-ux-expert (missing next action) are complementary: same line, different defect classes, both required.

Growth/positioning note: The oss-growth-hacker is correct that EMU and SSO credential friction is the highest-leverage silent blocker for enterprise adoption in the 0.11 cycle. Once the PR is clean, surface this as a named win for enterprise platform engineers: "APM now works out of the box in EMU orgs -- no credential gymnastics." Hold the release note language; do not publish it against a build that still has a fail-open path-existence gate.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class GitHubPackageDownloader {
        <<Facade>>
        +validate_virtual_package_exists(dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(dep_ref, path, ref, log) bool
        +_ref_exists_via_ls_remote(dep_ref, ref, log) bool
        +_ssh_attempt_allowed() bool
        +_resilient_get(url, headers, timeout) Response
        +_build_repo_url(repo_url, use_ssh, dep_ref, token, auth_scheme) str
        +_resolve_dep_token(dep_ref) str
        +_resolve_dep_auth_ctx(dep_ref) AuthContext
        +_sanitize_git_error(msg) str
    }

    class github_downloader_validation {
        <<Module / IOBoundary>>
        +validate_virtual_package_exists(downloader, dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(downloader, dep_ref, path, ref, log) bool
        +_ref_exists_via_ls_remote(downloader, dep_ref, ref, log) bool
        +_ssh_attempt_allowed(downloader) bool
        +_is_sha_pin(ref) bool
    }

    class AuthResolver {
        <<Strategy>>
        +resolve(host, owner, port) AuthContext
    }

    class AuthContext {
        <<ValueObject>>
        +token str
        +auth_scheme str
        +git_env dict
    }

    class DependencyReference {
        <<ValueObject>>
        +repo_url str
        +reference str
        +virtual_path str
        +host str
        +port int
        +is_virtual bool
        +is_virtual_collection() bool
        +is_virtual_file() bool
        +is_virtual_subdirectory() bool
        +is_azure_devops() bool
        +is_artifactory() bool
        +is_insecure bool
    }

    class InstallValidation {
        <<Orchestrator>>
        +validate_dependencies(deps) ValidationResult
    }

    class GitHubPackageDownloader:::touched
    class github_downloader_validation:::touched
    class InstallValidation:::touched

    GitHubPackageDownloader ..> github_downloader_validation : delegates (lazy import)
    GitHubPackageDownloader *-- AuthResolver : auth_resolver
    AuthResolver ..> AuthContext : returns
    github_downloader_validation ..> DependencyReference : reads
    github_downloader_validation ..> AuthContext : reads via downloader
    InstallValidation ..> GitHubPackageDownloader : calls validate_virtual_package_exists

    note for github_downloader_validation "Chain of Responsibility: marker-file probe -> Contents API -> git ls-remote"
    note for AuthResolver "Strategy: token -> credential-helper -> SSH"

    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A(["apm install owner/repo/sub#ref\nInstallValidation.validate_dependencies"])
    B["validate_virtual_package_exists\ngithub_downloader.py (thin shim)"]
    C["validate_virtual_package_exists\ngithub_downloader_validation.py"]
    D{dep_ref.is_virtual?}
    E(["raise ValueError"])
    F{"dep_ref type?"}

    G["[NET] _probe collection marker\nvpath.collection.yml @ ref"]
    H["[NET] _probe file\nvpath @ ref"]
    I["[NET] _probe marker files\napm.yml / SKILL.md / plugin.json..."]
    J{"any marker found?"}

    K["[NET] _directory_exists_at_ref\nContents API GET /repos/owner/repo/contents/path?ref=ref"]
    L{"HTTP 200?"}

    M{"dep_ref.reference set?"}
    N["[EXEC] _ref_exists_via_ls_remote\ngit ls-remote (3-attempt chain)"]
    O{"token available?"}
    P["[EXEC] git ls-remote authenticated HTTPS\ntoken URL + token_env"]
    Q["[EXEC] git ls-remote plain HTTPS\ncredential-helper env"]
    R{"SSH allowed?\n_ssh_attempt_allowed"}
    S["[EXEC] git ls-remote SSH\nssh -o BatchMode=yes"]
    T{"any attempt matched ref?"}

    U["warn_callback: path validation deferred\n[!] use --verbose"]
    V(["return True\n(defer path check to install)"])
    W(["return False"])
    X(["return True"])

    A --> B
    B --> C
    C --> D
    D -- No --> E
    D -- Yes --> F
    F -- collection --> G --> X
    F -- file --> H --> X
    F -- subdirectory --> I --> J
    J -- Yes --> X
    J -- No --> K --> L
    L -- Yes --> X
    L -- No --> M
    M -- No ref --> W
    M -- Has ref --> N
    N --> O
    O -- Yes --> P --> T
    O -- No --> Q --> T
    T -- No from HTTPS --> R
    R -- Yes --> S --> T
    T -- matched --> U --> V
    T -- no match --> W
Loading

Design patterns

  • Used in this PR: Chain of Responsibility -- validate_virtual_package_exists implements an explicit fallback chain (marker-file probe -> Contents API directory probe -> git ls-remote), mirroring the _clone_with_fallback auth chain.
  • Used in this PR: Facade (thin-shim delegation) -- GitHubPackageDownloader.validate_virtual_package_exists and the three private shims keep the class API stable while all logic lives in the extracted module.
  • Pragmatic suggestion: Protocol / structural subtyping -- introduce a small typing.Protocol (e.g., _ValidationDownloader) with only the handful of methods the validation module actually calls. This would break the implicit circular coupling, make the module independently testable with a simple stub, and eliminate the TYPE_CHECKING guard -- a concrete win at the current 335-line scope without over-engineering.

Required: 2 (see aggregated section above)
Nits: 3 (see aggregated section above)

CLI Logging Expert

Required: 1 (double [!] symbol when logger is present -- see aggregated section)
Nits: 2 (stale verbose hint; intent-before-outcome opening log line)

DevX UX Expert

Required: 2 (warning message gives no next action; apm-guide skill resources not updated -- see aggregated section)
Nits: 3 (CHANGELOG order; dense authentication.md paragraph; buried latency implication)

Supply Chain Security Expert

Required: 2 (fail-open path existence; unguarded virtual_path in Contents API URL -- see aggregated section)
Nits: 3 (repo_url unpack; SHA regex ambiguity; conflated warn_callback message)

Auth Expert

Required: 1 (bearer token embedded in URL instead of Authorization header for ADO -- see aggregated section)
Nits: 3 (resolve_for_dep vs resolve; TransportSelector not used; _sanitize_git_error gap)

OSS Growth Hacker

No findings.
Nits: 2 (CHANGELOG leads with mechanism; authentication.md missing troubleshooting cross-link)

Verdict computed deterministically: 8 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #941 · ● 2.8M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Round 2 of panel-review folding for microsoft#941. Addresses 8 findings on
SHA 7150c2a:

1. Full type annotations on github_downloader_validation public API
2. Length-guard on repo_url.split('/', 1) prevents ValueError
3. Strip [!] from warn message strings (logger prepends symbol)
4. Add recovery action to access-denied warning
5. Update packages/apm-guide/.apm/skills/apm-usage/commands.md
6. Close fail-open: shallow-fetch + ls-tree probes vpath after ls-remote
7. Apply validate_path_segments(vpath) before URL interpolation
8. ADO bearer tokens via http.extraHeader, not URL embedding

New regression tests cover findings 6, 7, 8.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Architecture direction is sound but three independent panelists flagged the same attempts[0] defect that breaks the core auth-chain promise, ADO PAT users hit a 401 regression, non-ADO tokens land in the OS process table, and the warn_callback message is wrong in three different ways.

Required before merge (12 items)

  • [python-architect] _path_exists_in_tree_at_ref always uses attempts[0], breaking the auth-chain fallback this PR was designed to fix at src/apm_cli/deps/github_downloader_validation.py:437

    • Why: _ref_exists_via_ls_remote iterates all attempts and returns on the first that resolves the ref. _path_exists_in_tree_at_ref then unconditionally picks attempts[0] (PAT/HTTPS). If ls-remote succeeded via SSH (attempt 2) because the PAT was rejected -- the EMU/SSO scenario this PR claims to fix -- the shallow-fetch fails with the PAT URL and returns False, restoring the original false-reject.
    • Suggested fix: Return the winning (label, url, env) tuple from _ref_exists_via_ls_remote and pass it to _path_exists_in_tree_at_ref, or iterate the chain inside _path_exists_in_tree_at_ref the same way _ref_exists_via_ls_remote does. Refactoring _ref_exists_via_ls_remote to return tuple[bool, tuple|None] costs one line and closes the bug cleanly.
  • [auth-expert] Same attempts[0] defect -- auth angle: the comment 'ls-remote caller already proved at least one works' is false when that one is not attempt 0; the warn_callback success branch is unreachable for EMU/SSO users at src/apm_cli/deps/github_downloader_validation.py

    • Why: If ls-remote succeeded on attempt N but _path_exists_in_tree_at_ref fails on attempt 0, the function returns False silently. The very population this PR targets (narrower API PAT than git credential helper) cannot benefit from the fallback as written.
  • [auth-expert] ADO auth scheme hardcoded to 'Bearer' in _build_validation_attempts -- 401 for every ADO PAT user at src/apm_cli/deps/github_downloader_validation.py

    • Why: ADO PATs require Authorization: Basic base64(:PAT), not Bearer <raw-PAT>. build_authorization_header_git_env("Bearer", dep_token) is called unconditionally regardless of dep_auth_scheme. _clone_with_fallback correctly gates on dep_auth_scheme == "bearer" before using dep_auth_ctx.git_env and includes a PAT->az-cli-bearer fallback; the validation path has neither guard.
    • Suggested fix: Gate on dep_auth_scheme: for "bearer" use dep_auth_ctx.git_env; for "basic" encode the PAT as base64(:PAT) and call build_authorization_header_git_env("Basic", encoded). Add the stale-PAT->az-cli-bearer fallback attempt that _clone_with_fallback provides at lines 726-774.
  • [supply-chain-security-expert] Non-ADO token embedded in git URL -- process-table and git-config exposure at src/apm_cli/deps/github_downloader_validation.py

    • Why: For non-ADO authenticated HTTPS, token=dep_token produces a credential-bearing URL visible in the OS process table during g.ls_remote(url, env=env) and written verbatim into the temp bare repo's .git/config via g.remote("add", "origin", url). ADO correctly uses build_authorization_header_git_env with an empty token URL; non-ADO must do the same. Threat-model item 6: credential exfiltration.
    • Suggested fix: Apply header-injection unconditionally for all HTTPS attempts: pass token="" to _build_repo_url and inject credentials via http.extraheader for every attempt.
  • [supply-chain-security-expert] robust_rmtree called directly in _path_exists_in_tree_at_ref -- bypasses safe_rmtree containment gate at src/apm_cli/deps/github_downloader_validation.py

    • Why: Every new file-deletion call MUST flow through path_security.safe_rmtree, which wraps robust_rmtree with an ensure_path_within containment assertion. Skipping the guard is a policy violation (threat-model item 7). Both base_temp and tmpdir are already in scope.
    • Suggested fix: Replace robust_rmtree(tmpdir) with safe_rmtree(tmpdir, base_temp).
  • [cli-logging-expert] Symbol mismatch: [i] used for both neutral probe misses and inconclusive ls-remote results at src/apm_cli/deps/github_downloader_validation.py

    • Why: The traffic-light rule maps [i] to expected/neutral info. Using it for ls-remote returned no SHA match via {label} (a degraded path toward fallback) breaks the "so what?" test -- users cannot distinguish expected absence from an auth probe producing no result.
    • Suggested fix: Use [!] for ls-remote returned no matching refs and no SHA match lines. Reserve [i] for genuinely neutral events (marker-file miss on the fast probe path). Use [x] (already used correctly) for hard errors.
  • [cli-logging-expert] _rich_echo called directly in _warn fallback in validation.py -- CommandLogger anti-pattern at src/apm_cli/install/validation.py

    • Why: Using _rich_* directly instead of CommandLogger in command functions is an explicit anti-pattern. The fallback bypasses CommandLogger lifecycle and DiagnosticCollector, making the warning invisible to structured log consumers and breaking quiet/verbose mode gating.
    • Suggested fix: Replace the _rich_echo fallback with a raise (programming error -- logger should always be present in the install call path) or route through DiagnosticCollector(category="warning").
  • [cli-logging-expert] warn_callback message does not name the package or ref that triggered the fallback at src/apm_cli/deps/github_downloader_validation.py

    • Why: A user installing multiple packages cannot tell which one triggered the warning. Violates the "name the thing" message-writing rule. Example: "Skipping my-skill -- local file exists" names it; "Path '...' validated via git fallback" does not include the full dep_ref string.
    • Suggested fix: Include the full dep_ref identifier (owner/repo/sub#ref) in the warn_callback message.
  • [devx-ux-expert] warn_callback fires on the happy path with implementation jargon at src/apm_cli/install/validation.py

    • Why: 'API probe was inconclusive', 'Contents-API scope', 'API gap' are opaque to users not reading GitHub API docs. Violates 'quiet on the happy path'. In CI, any stderr noise on a successful exit is a false alarm that can break pipelines or alert thresholds.
    • Suggested fix: Suppress entirely on the default path and surface only under --verbose, or reword to a single human-readable line: "Note: package access verified via git (API check was skipped). Run --verbose for details."
  • [devx-ux-expert] warn_callback displays package reference with '@' separator instead of '#' at src/apm_cli/install/validation.py

    • Why: The message renders the ref as owner/repo/sub@v1.2.0 but the CLI syntax the user typed is owner/repo/sub#v1.2.0. '@' is the version-pin separator in npm/pip/cargo; users will copy the '@' form and get an error.
    • Suggested fix: Derive the display string from dep_ref using the canonical '#' form.
  • [oss-growth-hacker] CHANGELOG entry written for maintainers, not users at CHANGELOG.md

    • Why: 'EMU/SSO users with a narrower env-var PAT' and 'lockfile-driven install would have handled silently' are internal implementation terms. A user who hit the error does not know what an env-var PAT is or why the lockfile path differs. The entry fails the one-line repost test.
    • Suggested fix: Lead with the user experience: "apm install no longer rejects packages you can actually access -- validation now uses the same credential chain as the actual install (token, git credential helper, SSH)."
  • [oss-growth-hacker] Always-visible warning message uses 'Contents-API scope' -- not a standard OAuth scope name at src/apm_cli/deps/github_downloader_validation.py

    • Why: 'verify your token's Contents-API scope' maps to no named OAuth scope a user can look up or add in the GitHub token settings UI. The message must name the exact scope string ('repo' or 'read:packages') or link to a doc page that does. Without a concrete action, the warning produces zero remediation.

Nits (16 items, skip if you want)

  • [python-architect] Backward-compat shims for private methods (_directory_exists_at_ref, _ref_exists_via_ls_remote, _ssh_attempt_allowed) on GitHubPackageDownloader are unnecessary -- no external caller should reference underscore-prefixed instance methods; only the public validate_virtual_package_exists delegation matters for test-mock compatibility.
  • [python-architect] _path_exists_in_tree_at_ref re-calls _build_validation_attempts even though _ref_exists_via_ls_remote already called it -- minor duplicated auth-resolution work.
  • [python-architect] The _log closure inside validate_virtual_package_exists is an unnecessary indirection; pass verbose_callback directly with an or (lambda _: None) default at the top of the function.
  • [python-architect] No test exercising the SSH-wins-but-PAT-fails auth-chain scenario; a parametrized test for that path would have caught the attempts[0] bug before merge.
  • [cli-logging-expert] [+] ls-remote ok via {label} (SHA match) -- the parenthetical '(SHA match)' is redundant; 'ok' already implies success.
  • [cli-logging-expert] Docs list probe types ('marker-file lookups', 'Contents API directory probe', 'git ls-remote fallback') but verbose log lines have no type prefix; consider [i] marker-file: ..., [i] contents-api: ... for alignment.
  • [devx-ux-expert] --verbose flag one-liner in cli-commands.md does not mention the new probe-chain output for #ref packages; append ", and validation probe chain for subdirectory packages".
  • [devx-ux-expert] authentication.md uses double hyphen as an em-dash inconsistently with surrounding prose style.
  • [supply-chain-security-expert] Comment in validate_virtual_package_exists says 'Empty / single-dot segments are tolerated' but validate_path_segments rejects single-dot segments; misleading to future security auditors.
  • [supply-chain-security-expert] _path_exists_in_tree_at_ref picks attempts[0] while _ref_exists_via_ls_remote iterates all -- corroborates the required finding above; listed as nit here to note supply-chain also flagged it independently.
  • [oss-growth-hacker] authentication.md: 'never rejects a package the lockfile-driven install would accept' is a double-negative that requires two passes to parse; 'always accepts what the actual install accepts' is one pass.
  • [oss-growth-hacker] commands.md: 'virtual subdirectory packages' is undefined at first use in the new section; an inline gloss '(packages specified as owner/repo/path#ref)' removes a lookup trip.
  • [oss-growth-hacker] commands.md: the --verbose tip is the most actionable line in the new section but placed last; move it to the top as a callout so users actively debugging see it first.
  • [auth-expert] _directory_exists_at_ref calls auth_resolver.resolve(host, owner, port=...) directly instead of auth_resolver.resolve_for_dep(dep_ref), violating the per-dep resolution rule.
  • [auth-expert] Authorization: f"token {token}" in _directory_exists_at_ref uses the legacy 'token' prefix for all token types; 'Bearer' is the RFC-correct form for OAuth/App tokens and is what GitHub docs now recommend.
  • [auth-expert] SSH attempt GIT_SSH_COMMAND has BatchMode=yes but no comment explaining that StrictHostKeyChecking is intentionally absent; add a comment so future editors do not add -o StrictHostKeyChecking=no thinking they are being helpful.

CEO arbitration

This PR lands a genuine auth-chain improvement -- credential-fallback sequencing for EMU/SSO users is the right problem to solve, and the architectural direction (Chain of Responsibility in _build_validation_attempts) is sound. However, three independent panelists (python-architect, auth-expert, supply-chain-security-expert) converged on the same structural defect: _path_exists_in_tree_at_ref hard-wires attempts[0], which breaks the exact guarantee the PR's headline makes. This is not a nit -- it is a logical contradiction embedded in the primary code path. The PR title says "auth-chain fallback"; the implementation falls back for ls-remote and then stops falling back for the tree-existence probe. The fix (return the winning attempt from _ref_exists_via_ls_remote, or iterate inside _path_exists_in_tree_at_ref) is well-specified and must land before merge. Separately, auth-expert's ADO Bearer-vs-Basic finding is a functional regression for every ADO PAT user -- a population the PR explicitly targets -- and supply-chain's token-in-URL finding is a credential-hygiene regression the PR introduces while adding new HTTPS attempt paths. These are not theoretical; they are new surface area this PR created. The safe_rmtree containment bypass is a policy violation, not a debate.

The warn_callback findings from cli-logging, devx-ux, and oss-growth-hacker are three lenses on the same UX failure: a warning fires on the happy path, uses implementation jargon the user cannot act on, references a non-standard "Contents-API scope" string that maps to no known OAuth scope, and uses '@' as a ref separator where the CLI contract is '#'. All three are required. The fact that three specialists reached this conclusion independently signals the warning was written for the engineer who wrote the code, not the user who reads it at 11pm wondering why their CI pipeline printed something alarming. The fix is a single, human-readable line with the '#' separator and a concrete scope string ('repo' or 'read:packages'), suppressed entirely on the default path and surfaced only under --verbose.

The CHANGELOG entry requires a rewrite. 'EMU/SSO users with a narrower env-var PAT' and 'lockfile-driven install would have handled silently' are maintainer notes, not user-facing copy. The rewrite angle is: APM now respects the same credential chain the user's git client already uses. No new configuration. That is the correct lede and it writes itself once the jargon is stripped.

Growth/positioning note: The underlying fix is a strong "it just works" story -- enterprise credential complexity (EMU, SSO, PAT scoping, SSH fallback) silently handled by APM itself rather than pushed onto the user. A clean CHANGELOG entry, once the required rewrite lands, is ready to lift into a release post with the frame: "apm now respects the same credential chain your git client uses -- nothing new to configure." That is a defensible, concrete differentiator. The headline writes itself.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class GitHubPackageDownloader {
      <<Downloader>>
      +validate_virtual_package_exists(dep_ref, verbose_callback, warn_callback) bool
      -_directory_exists_at_ref() bool
      -_ref_exists_via_ls_remote() bool
      -_ssh_attempt_allowed() bool
    }
    class GitHubPackageDownloaderValidation {
      <<Module>>
      +validate_virtual_package_exists(downloader, dep_ref, verbose_callback, warn_callback) bool
      +_build_validation_attempts(downloader, dep_ref, log) list
      +_ref_exists_via_ls_remote(downloader, dep_ref, ref, log) bool
      +_path_exists_in_tree_at_ref(downloader, dep_ref, vpath, ref, log) bool
      +_directory_exists_at_ref(downloader, dep_ref, path, ref, log) bool
      +_ssh_attempt_allowed(downloader) bool
      +_is_sha_pin(ref) bool
      +_split_owner_repo(repo_url) tuple
    }
    note for GitHubPackageDownloaderValidation "Chain of Responsibility:\nPAT HTTPS -> plain HTTPS (cred helper) -> SSH\nExtracted to respect 2400-line module cap"
    class DependencyReference {
      <<ValueObject>>
      +repo_url str
      +reference str
      +virtual_path str
      +is_virtual bool
    }
    class AuthResolver {
      <<Strategy>>
      +resolve(host, owner, port) AuthContext
    }
    class InstallValidation {
      <<Module>>
      +validate_package_exists(dep_ref, verbose_log, logger) bool
    }

    GitHubPackageDownloader ..> GitHubPackageDownloaderValidation : delegates (lazy import)
    GitHubPackageDownloaderValidation ..> DependencyReference : reads
    GitHubPackageDownloaderValidation ..> AuthResolver : reads via downloader
    InstallValidation ..> GitHubPackageDownloader : calls validate

    class GitHubPackageDownloaderValidation:::touched
    class GitHubPackageDownloader:::touched
    class InstallValidation:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A(["apm install owner/repo/sub#ref"])
    B["install/validation.py\nvalidate_virtual_package_exists(dep_ref,\n  verbose_callback, warn_callback)"]
    C{"dep_ref.is_virtual_subdirectory?"}
    D["[FS] validate_path_segments(vpath)\npath_security.py"]
    E["REJECT: PathTraversalError\nreturn False"]
    F["[NET] marker-file probes\ndownload_raw_file x7\n(apm.yml, SKILL.md, ...)"]
    G{"Any probe hit?"}
    H["return True"]
    I["[NET] _directory_exists_at_ref\nGET /repos/owner/repo/contents/path?ref=ref"]
    J{"HTTP 200?"}
    K{"dep_ref.reference is not None?"}
    L["[EXEC] _ref_exists_via_ls_remote\ngit ls-remote (up to 3 attempts)\nPAT -> plain HTTPS -> SSH"]
    M{"Ref found?"}
    N["[EXEC][FS] _path_exists_in_tree_at_ref\ngit init --bare + fetch --depth=1 --filter=tree:0\n+ git ls-tree FETCH_HEAD vpath\nBUG: only tries attempts[0] (PAT)"]
    O{"vpath in tree?"}
    P["warn_callback: 'validated via git fallback'\nreturn True"]
    Q["return False"]
    R["return False (no explicit ref)"]

    A --> B --> D
    D -->|"'..' detected"| E
    D -->|"clean"| C
    C -->|"no"| F2["[NET] file/collection probes\nreturn True/False"]
    C -->|"yes"| F
    F --> G
    G -->|"yes"| H
    G -->|"no"| I
    I --> J
    J -->|"200"| H
    J -->|"404 / error"| K
    K -->|"no"| R
    K -->|"yes"| L
    L --> M
    M -->|"no"| Q
    M -->|"yes"| N
    N --> O
    O -->|"yes"| P
    O -->|"no"| Q

    style N fill:#ffd6d6,stroke:#cc0000
    style E fill:#ffd6d6,stroke:#cc0000
Loading

Design patterns

  • Used in this PR: Chain of Responsibility -- _build_validation_attempts produces an ordered list[tuple[label, url, env]] that _ref_exists_via_ls_remote walks in priority order (PAT HTTPS -> plain HTTPS w/ credential helper -> SSH), short-circuiting on first success; mirrors _clone_with_fallback.
  • Pragmatic suggestion: Return-value pairing -- _ref_exists_via_ls_remote should return tuple[bool, tuple | None] (success flag + winning attempt) to eliminate the attempts[0] correctness bug without restructuring the call chain.

Required: see item 1 above.

Nits: backward-compat private-method shims unnecessary; _build_validation_attempts called twice; _log closure is unnecessary indirection; no SSH-wins-but-PAT-fails test.

CLI Logging Expert

Required: [i] symbol used for inconclusive ls-remote results (should be [!]); _rich_echo called directly in _warn fallback (CommandLogger anti-pattern); warn_callback message does not name the package/ref.

Nits: (SHA match) parenthetical in ls-remote success log is redundant; docs and verbose log probe-type labels are misaligned.

DevX UX Expert

Required: warn_callback fires on the happy path with opaque jargon (violates 'quiet on the happy path'; breaks CI pipelines); warn_callback uses '@' as ref separator instead of the CLI-canonical '#'.

Nits: --verbose one-liner in cli-commands.md does not mention probe-chain output for #ref packages; double hyphen used as em-dash in authentication.md.

Supply Chain Security Expert

Required: Non-ADO token embedded in git URL (credential-bearing URL visible in OS process table and written into temp bare repo .git/config; ADO uses header injection, non-ADO must too); robust_rmtree called directly instead of safe_rmtree (bypasses ensure_path_within containment gate).

Nits: misleading comment about single-dot segment tolerance in validate_path_segments; _path_exists_in_tree_at_ref picks attempts[0] -- corroborates python-architect required finding.

Auth Expert

Required: ADO auth scheme hardcoded to 'Bearer' -- ADO PATs require Basic base64(:PAT) not raw Bearer token; 401 regression for all ADO PAT users; missing PAT->az-cli-bearer fallback that _clone_with_fallback provides. Also: _path_exists_in_tree_at_ref uses attempts[0] -- the warn_callback success branch is unreachable for EMU/SSO users whose API PAT is rejected.

Nits: _directory_exists_at_ref calls auth_resolver.resolve() directly instead of resolve_for_dep(dep_ref); Authorization: f"token {token}" uses legacy prefix; SSH BatchMode=yes lacks explanatory comment for absent StrictHostKeyChecking override.

OSS Growth Hacker

Required: CHANGELOG entry uses internal jargon ('env-var PAT', 'lockfile-driven install') -- fails the repost test; warn message uses 'Contents-API scope' which is not a named OAuth scope users can find or add.

Nits: double-negative in authentication.md ('never rejects...would accept'); 'virtual subdirectory packages' undefined at first use in commands.md; --verbose tip buried at end of new commands.md section.

Verdict computed deterministically: 12 required findings across 6 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #941 · ● 4.3M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Round 3 panel surfaced 12 required findings across architect, auth,
supply-chain-security, cli-logging, devx-ux, and oss-growth personas.
This commit folds all 12 with regression tests appended.

Architecture / auth (the attempts[0] bug):
- _ref_exists_via_ls_remote now returns (bool, AttemptSpec | None) so the
  *winning* auth attempt survives the call. _path_exists_in_tree_at_ref
  takes that winning_attempt and uses the same env+url for the tree probe.
  No more fanning the path probe at attempts[0].
- New AttemptSpec NamedTuple (label, url, env) is the carrier between
  ref check and path probe.

ADO auth correctness:
- ADO PATs now use HTTP Basic with base64(':<PAT>') (the leading colon
  is the ADO convention). Bearer fallback for AAD tokens preserved.
- Non-ADO HTTPS attempts inject the token via http.extraheader, never
  via the URL. Closes 'token in process argv' leak.

Security:
- Tree-probe cleanup uses safe_rmtree(tmpdir, base_temp), never the
  raw robust_rmtree -- matches the project-wide path-safety rule.

CLI logging:
- All ls-remote 'no match' / 'tree miss' messages use [!] (was [i]).
- _warn in install/validation.py is verbose-gated and no longer falls
  back to _rich_echo (logger is always present in install path).

DevX:
- Warn message now reads
  'Validated <ref> via git fallback (API check skipped). Run with --verbose for details.'
  using DependencyReference.to_canonical() (# separator).

CHANGELOG:
- microsoft#941 entry rewritten in user voice.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Two supply-chain security bugs introduced by this PR directly contradict documented invariants; cli-logging and DevX UX converge on a silent-fallback issue; architecture and auth chain are sound.

Required before merge (7 items)

  • [supply-chain-security-expert] Empty-string ref bypasses the explicit-ref gate and activates git fallback against 'main' at src/apm_cli/deps/github_downloader_validation.py:182

    • Why: Line 123 computes ref = dep_ref.reference or "main" (empty string coerces to "main") while line 182 gates on dep_ref.reference is not None (empty string is not None). A bare # fragment produces reference=="" which passes the gate and activates the git fallback against main. This directly contradicts the documented invariant that the git fallback is only reachable for explicitly pinned refs.
    • Suggested fix: Change if dep_ref.reference is not None: to if dep_ref.reference: (truthy check). One-character fix, but correctness consequence is significant.
  • [supply-chain-security-expert] validate_path_segments called without reject_empty allows empty vpath to reach git ls-tree at src/apm_cli/deps/github_downloader_validation.py:130

    • Why: An empty string passes the guard (not in the reject set {"..", "."}). git ls-tree FETCH_HEAD "" behavior is implementation-defined; some git versions treat it as a root-listing and emit non-empty output, making _path_exists_in_tree_at_ref return True for any successfully fetched repo regardless of virtual path.
    • Suggested fix: Add if not vpath: return False immediately before validate_path_segments, or pass reject_empty=True to the existing call.
  • [cli-logging-expert] warn_callback is suppressed in non-verbose mode, hiding actionable fallback signal from default users at src/apm_cli/install/validation.py

    • Why: The fallback message "Validated X via git fallback (API check skipped). Run with --verbose for details." is explicitly actionable. Suppressing it in non-verbose mode means default-mode CI runs silently hit the git fallback with no indication. The "Run with --verbose for details" suffix is only meaningful if the message is shown; hiding it defeats its purpose.
    • Suggested fix: Show the warn_callback message in non-verbose mode (it is a warning, not verbose detail). Strip the "Run with --verbose for details" suffix only when verbose is already True.
  • [cli-logging-expert] _warn silently drops the warning when logger is None outside unit tests at src/apm_cli/install/validation.py

    • Why: The comment "A None logger here means the caller is a unit test" is an assertion without enforcement. If any production caller constructs the validator without a logger, the warning is silently swallowed. Silent drops of warnings violate the traffic-light rule: yellow must reach the user.
    • Suggested fix: Add assert logger is not None before the warning block, or add a _rich_echo fallback path.
  • [devx-ux-expert] Silent git-fallback in non-verbose mode violates the "flags that change behavior without telling the user" anti-pattern at src/apm_cli/install/validation.py

    • Why: When the API rejects a package but the git fallback accepts it, the user and CI operator receive no signal. In CI this is security-relevant: a scoped PAT may have correctly rejected a package the user should not access. Silently succeeding means operators cannot distinguish legitimate access from credential scope mismatch.
    • Suggested fix: Emit a non-verbose stderr warning when the git fallback resolves a package the token probe rejected: "warning: API validation skipped for (pkg); resolved via git credential fallback. Run with --verbose for details."
  • [devx-ux-expert] No defined error message when ALL probes fail -- "Failure mode is the product" rule violated at packages/apm-guide/.apm/skills/apm-usage/commands.md

    • Why: The PR documents the four-step fallback chain but never specifies what the user sees when every step fails. Every error must name what failed, why, and one concrete next action. Without a defined, user-facing terminal error message, the failure UX is undefined and untestable.
    • Suggested fix: Document (and implement) a terminal error: "error: could not validate (pkg) -- all probes failed (marker-file, Contents API, git ls-remote, shallow-fetch). Verify the package path and ref exist and that your credentials have read access. Run apm install --verbose (pkg) for the full probe log."
  • [devx-ux-expert] commands.md validation-chain section is too deep for a quick-reference skill doc and breaks discoverability at packages/apm-guide/.apm/skills/apm-usage/commands.md

    • Why: A four-step numbered implementation walkthrough belongs in a troubleshooting or internals reference, not the primary commands reference that surfaces in agent flows. Users consulting commands.md during an install failure will be overwhelmed before reaching the actionable part.
    • Suggested fix: Collapse to one sentence with a link: "apm install validates subdirectory packages before writing to apm.yml using the same credential chain as the actual install (see Authentication > Install validation chain for details)." Move the four-step breakdown to the authentication reference or a troubleshooting page.

Nits (18 items, skip if you want)

  • [python-architect] warn_callback docstring contradicts actual wiring -- docstring says the callback fires in non-verbose mode but install/validation.py gates it on if not verbose: return
  • [python-architect] Legacy test suite exercises _ref_exists_via_ls_remote via the bool-only shim; no test asserts that the follow-up probe reuses the winning AttemptSpec rather than attempts[0]
  • [cli-logging-expert] [!] used for ls-remote no-matching-refs; [i] would be more accurate -- probe miss is informational, not user-actionable at this stage
  • [cli-logging-expert] Verbose block lacks a terminal outcome line summarizing pass/fail; reader must parse the sequence to determine the outcome
  • [devx-ux-expert] CHANGELOG entry says "same credential chain as the actual install" -- slightly imprecise; the shallow-fetch step has no analog in a normal install
  • [devx-ux-expert] authentication.md new paragraph uses double-negative ("never rejects a package the lockfile-driven install would accept") -- harder to parse than needed
  • [devx-ux-expert] --verbose description in cli-commands.md lists three probe steps but omits the shallow-fetch/ls-tree step; inconsistent with the four-step description in commands.md
  • [supply-chain-security-expert] StrictHostKeyChecking safety is comment-only; no test prevents a future developer from adding -o StrictHostKeyChecking=no to GIT_SSH_COMMAND
  • [supply-chain-security-expert] Git fallback availability is credential-dependent, weakening lockfile determinism across environments; document in security.md that CI must supply the same credential chain as developer machines
  • [supply-chain-security-expert] warn_callback=None default silently swallows the git-fallback audit trail; all production call sites should supply a warn_callback so the bypass is always visible to the operator
  • [oss-growth-hacker] CHANGELOG headline buries the audience; enterprise/SSO/EMU users won't self-identify from the current phrasing
  • [oss-growth-hacker] authentication.md new paragraph has no runnable example showing the before/after scenario
  • [oss-growth-hacker] No release beat planned; enterprise friction removal is a high-signal story for the "works in real orgs" narrative
  • [oss-growth-hacker] First-run failure mode is still silent for users who hit this pre-fix; no re-engagement path for churned evaluators
  • [oss-growth-hacker] Module extraction is a compounding win worth naming in the CHANGELOG as a foundation for future credential-provider extensibility
  • [auth-expert] GHES Contents-API URL else branch is dead code -- is_github_hostname returns False for GHES instances so the branch is unreachable; GHES silently falls through to ls-remote
  • [auth-expert] SHA prefix startswith match has no minimum-length guard; 7-char abbreviated SHAs could theoretically prefix-match two objects in a very large repo
  • [auth-expert] Mermaid flowchart in authentication.md does not show the new validation fallback chain; per Rule 4, the diagram must stay in sync with behavior

CEO arbitration

Two supply-chain-security findings are the sharpest blockers and must be addressed before merge. The empty-string ref bypass and the empty-vpath reaching git ls-tree are both correctness bugs introduced by this PR that directly contradict documented security invariants. Both are in the new validation module and must be fixed here. The fixes are small (one-character change for the ref gate; a null guard for the empty vpath), but the correctness consequences are significant -- an attacker who controls a dep string could add a trailing # to force git-credential-based validation on any package that failed the API check, and an empty vpath could validate as present for any repo.

cli-logging-expert and devx-ux-expert converge from different angles on the same silent-fallback problem. cli-logging-expert frames it as a traffic-light violation: actionable fallback output is suppressed below --verbose. devx-ux-expert frames it as a UX anti-pattern: flags that change behavior without telling the user. The two framings are consistent, not contradictory, and together make the case that a non-verbose stderr warning is required when the git fallback resolves a package the token probe rejected. devx-ux-expert additionally raises that the all-probes-fail error shape is unspecified and that commands.md has absorbed implementation detail that belongs in a troubleshooting reference.

python-architect found the module decomposition sound and raised no required findings; the extraction of validation logic into github_downloader_validation.py is affirmed. auth-expert found no blocking authentication issues -- the ADO basic/bearer distinction, the token injection via http.extraheader, the winning-attempt forwarding for the shallow-fetch probe, and the SHA-pin handling are all correct. The GHES dead-code branch and the missing Mermaid update are nits only. The panel is not divided on the verdict; the required findings are additive and mutually reinforcing.

Dissent resolved: There is no dissent between panelists. python-architect's nit on the warn_callback docstring is consistent with cli-logging-expert's required finding on suppression behavior -- both point at the same mismatch, python-architect at the documentation gap and cli-logging-expert at the runtime consequence. oss-growth-hacker's note does not conflict with any security or UX finding; it is purely additive framing for post-merge positioning.

Growth/positioning note: Once the security and UX blockers are resolved, the release note should lead with the user-facing outcome: "apm install now works with your existing credential chain, including SSO, EMU, and GHES tokens." Enterprise and EMU credential friction is a documented evaluation-stage drop-off point for internal tooling. A short Discussions post asking whether users hit silent install rejections before this fix would surface churned evaluators and provide organic signal for the roadmap. The module extraction to github_downloader_validation.py is also worth naming explicitly in the CHANGELOG as a foundation for future credential-provider extensibility.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class GitHubPackageDownloader {
        <<Facade>>
        +validate_virtual_package_exists(dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(dep_ref, path, ref, log) bool
        +_ref_exists_via_ls_remote(dep_ref, ref, log) bool
        +_ssh_attempt_allowed() bool
        +download_raw_file(dep_ref, path, ref)
        +_resilient_get(url, headers, timeout) Response
        -auth_resolver AuthResolver
        -git_env dict
    }
    class github_downloader_validation {
        <<Module>>
        +validate_virtual_package_exists(downloader, dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(downloader, dep_ref, path, ref, log) bool
        +_ref_exists_via_ls_remote(downloader, dep_ref, ref, log) tuple
        +_path_exists_in_tree_at_ref(downloader, dep_ref, vpath, ref, log, winning) bool
        +_build_validation_attempts(downloader, dep_ref, log) list
        +_ssh_attempt_allowed(downloader) bool
    }
    class AttemptSpec {
        <<ValueObject>>
        +label str
        +url str
        +env dict
    }
    class AuthResolver {
        <<Strategy>>
        +resolve(host, owner, port) AuthContext
    }
    class DependencyReference {
        <<ValueObject>>
        +repo_url str
        +virtual_path str
        +reference str
        +is_virtual_subdirectory() bool
        +is_azure_devops() bool
    }
    class GitHubPackageDownloader:::touched
    class github_downloader_validation:::touched
    class AttemptSpec:::touched
    GitHubPackageDownloader ..> github_downloader_validation : delegates (lazy import)
    github_downloader_validation *-- AuthResolver : resolves via downloader
    github_downloader_validation ..> AttemptSpec : produces chain
    github_downloader_validation ..> DependencyReference : reads
    GitHubPackageDownloader *-- AuthResolver : auth_resolver
    note for github_downloader_validation "Chain of Responsibility: token HTTPS -> plain HTTPS -> SSH via AttemptSpec list"
    note for GitHubPackageDownloader "Shim methods: backward-compat delegation so existing mocks keep working"
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A[apm install CLI] --> B[_validate_package_exists\ninstall/validation.py]
    B --> C[GitHubPackageDownloader\n.validate_virtual_package_exists shim]
    C --> D[gdv.validate_virtual_package_exists\ngithub_downloader_validation.py]
    D --> SEC{validate_path_segments\nvpath traversal check}
    SEC -- PathTraversalError --> REJECT[return False]
    SEC -- ok --> TYPE{dep_ref type?}
    TYPE -- subdirectory --> MF[[NET] marker-file probes\napm.yml SKILL.md plugin.json README.md]
    MF -- any hit --> RT[return True]
    MF -- all miss --> DIR[[NET] _directory_exists_at_ref\nContents API GET /repos/.../contents/vpath]
    DIR -- 200 --> RT
    DIR -- 404/err --> REF{dep_ref.reference\nnot None?}
    REF -- no ref --> RF[return False]
    REF -- has ref --> LS[[EXEC] _ref_exists_via_ls_remote\ngit ls-remote via AttemptSpec chain\ntoken-HTTPS -> plain-HTTPS -> SSH]
    LS -- all fail --> RF
    LS -- ok, winning_attempt --> TREE[[EXEC][FS] _path_exists_in_tree_at_ref\ngit init bare + fetch depth=1\nfilter=tree:0 + ls-tree FETCH_HEAD vpath]
    TREE -- vpath present --> WARN[warn_callback fired if caller wires it] --> RT
    TREE -- vpath absent --> RF
Loading

Design patterns

  • Used in this PR: Chain of Responsibility -- _build_validation_attempts constructs an ordered list[AttemptSpec] (token-HTTPS -> plain-HTTPS -> SSH); _ref_exists_via_ls_remote walks it and short-circuits on the first success, returning the winning node so follow-up probes reuse the same auth.
  • Used in this PR: Dataclass-as-value-object -- AttemptSpec(NamedTuple) is an immutable value object carrying (label, url, env). NamedTuple is the right choice; the tuple is consumed immediately and never mutated.
  • Used in this PR: Adapter/shim -- the four methods on GitHubPackageDownloader are backward-compat thin adapters translating the old (dep_ref, ref, log) -> bool surface to the new (downloader, dep_ref, ref, log) -> tuple[bool, AttemptSpec|None] surface.
  • Pragmatic suggestion: none -- the current shape is the simplest correct design at this scope.

CLI Logging Expert

Required findings (see above).

Nits: [!] used for ls-remote no-matching-refs should be [i] (probe miss is informational, not user-actionable at this stage). Verbose block lacks a terminal outcome line summarizing pass/fail.

DevX UX Expert

Required findings (see above).

Nits: CHANGELOG "same credential chain" is slightly imprecise (shallow-fetch has no analog in a normal install). authentication.md new paragraph uses double-negative. --verbose description in cli-commands.md omits the shallow-fetch/ls-tree step.

Supply Chain Security Expert

Required findings (see above).

Nits: StrictHostKeyChecking safety is comment-only with no test enforcement. Git fallback availability is credential-dependent, weakening lockfile determinism across environments; document in security.md. warn_callback=None default silently swallows the git-fallback audit trail at non-verbose log levels.

Auth Expert

No findings.

Nits: GHES Contents-API URL else branch is dead code (is_github_hostname returns False for GHES, so the branch is unreachable). SHA prefix startswith match has no minimum-length guard. Mermaid flowchart in authentication.md does not show the new validation fallback chain.

OSS Growth Hacker

No findings.

Nits: CHANGELOG headline buries the enterprise/SSO/EMU audience. authentication.md new paragraph has no runnable example. No release beat planned. No re-engagement path for users who hit this pre-fix. Module extraction is a compounding win worth naming in the CHANGELOG.

Verdict computed deterministically: 7 required findings across 6 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #941 · ● 2.7M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
…osed

Round 4 of panel fold for microsoft#941. 7 required findings on 4fdc431:
- supply-chain (2): empty-ref truthy gate; reject_empty=True on vpath
- cli-logging (2): warn_callback surfaced in non-verbose; _rich_warning
  fallback when logger is None; suffix-stripping symmetry
- devx-ux (3): named four-probe chain in terminal error; commands.md
  collapsed to one-sentence link; validation chain detail in
  authentication.md

Validated locally via 6-persona panel + apm-ceo synthesizer (APPROVE).
Lint clean. Full unit suite: 6933 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Architecture and auth chain alignment are sound; three required changes block merge -- two user-message rewrites and one AuthResolver API violation.

Required before merge (3 items)

  • [devx-ux-expert] Error message lists internal probe names instead of user-actionable guidance at src/apm_cli/commands/install.py

    • Why: "all probes failed (marker-file, Contents API, git ls-remote, shallow-fetch)" is a prose stack trace. The parenthetical names APM internals that mean nothing to a user and violates the persona anti-pattern "help text written for maintainers, not users." The concrete guidance ("verify the path and ref exist and that your credentials have read access") also fuses two separate actions.
    • Suggested fix: "Package path not found or not accessible. Check that the path and ref are correct, then verify your credentials can read the repository. Run with --verbose for a detailed probe log."
  • [devx-ux-expert] Yellow warning on the git-fallback happy path uses implementation jargon at src/apm_cli/commands/install.py

    • Why: "API validation skipped for {pkg}; resolved via git credential fallback" leaves a user who sees yellow on a successful install unsure whether their install is trustworthy or whether they must act. "API validation skipped" and "git credential fallback" are implementation terms, not user outcomes.
    • Suggested fix: "Could not validate {pkg} via GitHub API; fell back to git-based auth and succeeded. Run with --verbose to review what was checked."
  • [auth-expert] _directory_exists_at_ref calls downloader.auth_resolver.resolve(host, owner, port=dep_ref.port) directly instead of downloader.auth_resolver.resolve_for_dep(dep_ref) at src/apm_cli/deps/github_downloader_validation.py:253

    • Why: The AuthResolver architecture invariant (auth.py docstring, auth-expert rule Integrate copilot runtime #2) requires resolve_for_dep(dep_ref) for all per-dep operations -- never a direct resolve() call. Functionally equivalent today because _split_owner_repo(dep_ref.repo_url)[0] equals what resolve_for_dep extracts as org, but if resolve_for_dep gains per-dep enrichment (ADO org disambiguation, port-aware org overrides) this call site silently falls behind.
    • Suggested fix: Replace downloader.auth_resolver.resolve(host, owner, port=dep_ref.port).token with downloader.auth_resolver.resolve_for_dep(dep_ref).token

Nits (13 items, skip if you want)

  • [python-architect] AttemptSpec.env typed as bare dict; tighten to dict[str, str] -- every call site constructs a str->str mapping and the loose annotation hides accidental non-string injection
  • [python-architect] _ref_exists_via_ls_remote backward-compat shim docstring should explicitly state "returns bool; winning AttemptSpec is discarded" so a future reader doesn't assume the old API returned a tuple
  • [python-architect] Module-level functions taking downloader as first arg: a VirtualPackageValidator(downloader) dataclass would make the collaborator boundary explicit -- worth considering at next natural refactor point
  • [cli-logging-expert] validation.py _warn: suffix-stripping relies on exact msg.endswith(" Run with --verbose for details.") -- fragile; prefer a boolean include_verbose_hint parameter to _warn
  • [cli-logging-expert] validation.py _warn: _rich_warning fallback when logger=None is the _rich_*-direct anti-pattern but is the only viable option; document that logger=None is for test/library call paths only, not from any command function
  • [cli-logging-expert] warn_callback message text does not include [!] prefix in the string itself -- depends on CommandLogger to prepend it; tighten the logger parameter type annotation to CommandLogger subtype, not stdlib logger
  • [devx-ux-expert] "probe log" is still technical jargon; prefer "Run with --verbose to see each check that was attempted"
  • [devx-ux-expert] Recovery UX gap: --verbose shows the probe log but the error message gives no hint what the user does after reading it; consider appending a canonical hint like git ls-remote <repo> so the user has one copy-paste action to self-diagnose
  • [supply-chain-security-expert] verbose_callback logs dep_ref.repo_url directly; if a user misconfigures a dep with a credential in the URL field, that token appears in verbose output -- a _sanitize_git_error-style scrub would be belt-and-suspenders
  • [supply-chain-security-expert] contextlib.suppress(Exception) on safe_rmtree silently swallows all cleanup failures (disk-leak, not credential-leak); a best-effort log on cleanup failure would let operators find orphaned temp dirs under get_apm_temp_dir()
  • [supply-chain-security-expert] _build_validation_attempts returns [] for Artifactory with no comment explaining why -- add one line making the deliberate skip auditable
  • [auth-expert] SSH BatchMode=yes in a fresh CI environment where the host key has never been accepted fails outright with no diagnostic hint; a log line flagging "SSH attempt failed -- run manually once to accept host key" on the ssh non-zero exit would save significant user debugging time at src/apm_cli/deps/github_downloader_validation.py:385
  • [oss-growth-hacker] The "Yellow signal" section in authentication.md explains what the warning means but lacks an explicit CTA -- a first-run enterprise dev may stall at the warning even though install succeeded; add one sentence on what to do next

CEO arbitration

The panel converges on a sound architecture: the four-step validation probe chain correctly mirrors the actual install auth chain, the AttemptSpec NamedTuple is a clean value-object boundary, and the lazy-import shim preserves backward compatibility for existing call sites. The extraction of github_downloader_validation.py is approved in principle by all panelists; no specialist disputes the structural direction.

Two blockers must be resolved before merge. First, devx-ux-expert flags both the failure message and the yellow-path warning as maintainer-oriented prose masquerading as user guidance. These are not style nits -- user-facing message quality is a correctness requirement under APM's enterprise-evaluator audience. Second, auth-expert identifies a violated architecture invariant at line 253: _directory_exists_at_ref calls downloader.auth_resolver.resolve(host, owner, port=dep_ref.port) directly instead of downloader.auth_resolver.resolve_for_dep(dep_ref). Per the auth.py docstring contract, resolve_for_dep is the required entry point for per-dep operations; direct resolve() calls silently bypass any future per-dep enrichment. The fix is one line and must ship with this PR.

All remaining findings from python-architect, cli-logging-expert, supply-chain-security-expert, and oss-growth-hacker are nits: tightening AttemptSpec.env typing, the shim docstring gap, the suffix-stripping fragility in _warn, the URL sanitize scrub on verbose logs, null-byte and percent-encoding comments in validate_path_segments, the suppress(Exception) on safe_rmtree, and the missing Artifactory skip rationale comment. These are quality-of-life improvements encouraged for an immediate follow-up but do not block merge once the three required findings are addressed.

Dissent resolved: No inter-panelist dissent on required vs nit classification. All three required findings were raised by a single specialist and not contradicted by any other panelist. No arbitration tiebreak was needed.

Growth/positioning note: oss-growth-hacker correctly identifies this as the highest-friction enterprise first-run failure -- a user's first apm install silently rejecting a valid package because the API probe used narrower auth than the actual clone is silent churn at the top of the enterprise evaluation funnel. CHANGELOG and release comms should lead with "enterprise SSO/EMU now just works on first install" and may additionally surface the github_downloader_validation.py split as the extensibility hook that enables a future "bring your own credential provider" story.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class GitHubPackageDownloader {
        <<IOBoundary>>
        +validate_virtual_package_exists(dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(...) bool
        +_ref_exists_via_ls_remote(...) bool
        +_ssh_attempt_allowed() bool
        +download_raw_file(...)
        +_build_noninteractive_git_env(...) dict
    }
    note for GitHubPackageDownloader "Shim layer: all validation methods
are now thin delegators to
github_downloader_validation module"

    class github_downloader_validation {
        <<Pure>>
        +validate_virtual_package_exists(downloader, dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(downloader, dep_ref, vpath, ref, log) bool
        +_build_validation_attempts(downloader, dep_ref, log) list
        +_ref_exists_via_ls_remote(downloader, dep_ref, ref, log) tuple
        +_path_exists_in_tree_at_ref(downloader, dep_ref, vpath, ref, log, winning_attempt) bool
        +_ssh_attempt_allowed(downloader) bool
    }
    note for github_downloader_validation "Chain of Responsibility:
marker-file -> Contents API ->
git ls-remote -> shallow-fetch+ls-tree"

    class AttemptSpec {
        <<ValueObject>>
        +label str
        +url str
        +env dict
    }

    class DependencyReference {
        <<ValueObject>>
        +is_virtual bool
        +reference str
        +virtual_path str
        +repo_url str
        +is_virtual_file() bool
        +is_virtual_collection() bool
        +is_virtual_subdirectory() bool
        +is_azure_devops() bool
    }

    class InstallValidation {
        <<IOBoundary>>
        +_validate_package_exists(dep_ref, ...) bool
    }

    class AuthResolver {
        <<Strategy>>
        +resolve_for_dep(dep_ref) AuthContext
    }

    InstallValidation ..> GitHubPackageDownloader : constructs + calls
    GitHubPackageDownloader ..> github_downloader_validation : lazy import delegates
    github_downloader_validation *-- AttemptSpec : produces chain
    github_downloader_validation ..> DependencyReference : reads
    github_downloader_validation ..> GitHubPackageDownloader : first-arg collaborator
    GitHubPackageDownloader o-- AuthResolver : injected

    class GitHubPackageDownloader:::touched
    class github_downloader_validation:::touched
    class InstallValidation:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A(["apm install\n(CLI entry)"])
    B["install/validation.py:\n_validate_package_exists"]
    C["[FS] Construct GitHubPackageDownloader\nwith auth_resolver"]
    D["GitHubPackageDownloader.validate_virtual_package_exists\n(thin shim -- lazy import)"]
    E["github_downloader_validation.validate_virtual_package_exists"]
    F{"validate_path_segments\n(vpath, reject_empty=True)"}
    G(["return False\n(PathTraversalError)"])
    H{"dep_ref type?"}
    I["[NET] _probe: download_raw_file\nmarker-file (collection.yml / file / apm.yml...)"]
    J{"marker hit?"}
    K(["return True"])
    L["[NET] _directory_exists_at_ref\nContents API GET /repos/{owner}/{repo}/contents/{vpath}"]
    M{"API 200?"}
    N{"dep_ref.reference\ntruthy?"}
    O(["return False"])
    P["[EXEC] _build_validation_attempts\nbuild AttemptSpec chain\n(token-header / plain-HTTPS / SSH)"]
    Q["[NET][EXEC] _ref_exists_via_ls_remote\ngit ls-remote per AttemptSpec"]
    R{"ref found?\nwinning_attempt != None?"}
    S["[FS][EXEC] _path_exists_in_tree_at_ref\ngit fetch --depth=1 + ls-tree FETCH_HEAD vpath\n(uses WINNING AttemptSpec -- not attempts[0])"]
    T{"ls-tree hit?"}
    U["warn_callback fired\n(yellow: git-cred chain wider than API PAT)"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F -- "traversal / empty" --> G
    F -- "ok" --> H
    H -- "collection" --> I
    H -- "file" --> I
    H -- "subdirectory" --> I
    I --> J
    J -- "yes" --> K
    J -- "no (all markers missed)" --> L
    L --> M
    M -- "yes" --> K
    M -- "no" --> N
    N -- "no (default branch)" --> O
    N -- "yes" --> P
    P --> Q
    Q --> R
    R -- "no" --> O
    R -- "yes" --> S
    S --> T
    T -- "no" --> O
    T -- "yes" --> U
    U --> K
Loading

Design patterns

  • Used in this PR: Chain of Responsibility -- _build_validation_attempts constructs a linear auth-attempt chain (token-header -> plain-HTTPS-credential-helper -> SSH); each link is tried in order and the first success propagates as the winning_attempt.
  • Used in this PR: ValueObject (NamedTuple) -- AttemptSpec is an immutable, typed record flowing through the probe chain; replaces implicit positional tuples and makes the winning-attempt hand-off to _path_exists_in_tree_at_ref explicit and safe.
  • Used in this PR: Adapter (shim) -- GitHubPackageDownloader._ref_exists_via_ls_remote adapts the new tuple[bool, AttemptSpec | None] return signature back to the bool legacy call sites expected.
  • Pragmatic suggestion: VirtualPackageValidator(downloader: GitHubPackageDownloader) dataclass -- converting the module-level functions that take downloader as their first argument into a small dataclass would make the collaborator boundary explicit and simplify test setup; net readability gain with zero added abstraction layers.

No findings.

CLI Logging Expert

No findings.

DevX UX Expert

Required:

  • Error message "all probes failed (marker-file, Contents API, git ls-remote, shallow-fetch)" lists APM internals as a prose stack trace. Suggested rewrite: "Package path not found or not accessible. Check that the path and ref are correct, then verify your credentials can read the repository. Run with --verbose for a detailed probe log."
  • Yellow warning "API validation skipped for {pkg}; resolved via git credential fallback" uses implementation jargon on the happy path. Suggested rewrite: "Could not validate {pkg} via GitHub API; fell back to git-based auth and succeeded. Run with --verbose to review what was checked."

Nits:

  • "probe log" is still technical jargon; prefer "Run with --verbose to see each check that was attempted."
  • Recovery UX gap: error message gives no hint what the user does after reading the verbose log; consider a canonical git ls-remote <repo> hint.

Supply Chain Security Expert

No findings.

Auth Expert

Required:

  • _directory_exists_at_ref at line 253 calls downloader.auth_resolver.resolve(host, owner, port=dep_ref.port) instead of downloader.auth_resolver.resolve_for_dep(dep_ref), violating the per-dep auth resolution invariant. Fix: downloader.auth_resolver.resolve_for_dep(dep_ref).token.

Nits:

  • SSH BatchMode=yes in CI with an untrusted host key fails with no diagnostic; add a [!] log hint on ssh non-zero exit pointing to the host-key cause.
  • dep_ref.repo_url is always owner/repo format (no embedded credentials); the verbose log line at line 156 is safe.

OSS Growth Hacker

No findings.

Verdict computed deterministically: 3 required findings across 2 active panelists (devx-ux-expert: 2, auth-expert: 1). APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #941 · ● 3.4M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Replace direct resolve(host, owner, port=...) call with the per-dep
API resolve_for_dep(dep_ref). Functionally equivalent today, but keeps
this call site aligned with the AuthResolver invariant so future
per-dep enrichment (ADO org disambiguation, port-aware org overrides)
flows through automatically.

Round 5 panel finding (auth-expert).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added panel-review Trigger the apm-review-panel gh-aw workflow and removed panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. labels Apr 30, 2026
@danielmeppiel danielmeppiel removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel merged commit 231c0a4 into microsoft:main Apr 30, 2026
10 checks passed
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Architecture is sound and the enterprise auth problem is real, but two auth-chain correctness gaps (ADO bearer fallback missing, PAT over plain HTTP), two user-facing messaging regressions, and one editorial CHANGELOG issue block merge.

Required before merge (5 items)

  • [devx-ux-expert] Probe mechanism names in default (non-verbose) error output are too internal at src/apm_cli/commands/install.py

    • Why: The default error message exposes "marker-file, Contents API, git ls-remote, shallow-fetch" -- implementation-level probe names that mean nothing to a user who typed apm install owner/repo/sub#branch. npm, pip, and cargo never surface internal resolution-mechanism names in default output. The probe list belongs exclusively in --verbose output.
    • Suggested fix: Default message: "'owner/repo/sub@branch' could not be resolved -- verify the path, ref, and credentials (run with --verbose for the full probe log)". Reserve the probe-name list for --verbose output only.
  • [devx-ux-expert] Yellow warning "API validation skipped ... resolved via git credential fallback" fires on a success path and uses internal vocabulary at src/apm_cli/install/validation.py

    • Why: Users see "API validation skipped" and conclude something went wrong; in reality install will succeed. "git credential fallback" is internal plumbing jargon a user who configured gh auth login has no reason to know. A warning-level signal on the success path causes warning fatigue. If the signal must exist, it should be [i] info level.
    • Suggested fix: Demote to [i] info level. Reword: "{pkg} resolved using system credentials (git credential helper)". Drop "API validation skipped" framing entirely.
  • [auth-expert] ADO stale-PAT AAD bearer fallback missing from _build_validation_attempts at src/apm_cli/deps/github_downloader_validation.py:336

    • Why: _clone_with_fallback silently retries with az-cli AAD bearer when ADO PAT is rejected with 401. _build_validation_attempts builds only 3 attempts (PAT header, credential helper, SSH) with no 401-catch-and-bearer-retry. When ADO_APM_PAT is stale but az is available, install succeeds (bearer fallback) while validation fails (no such retry). This directly falsifies the auth doc's claim: "apm install from the CLI never rejects a package the lockfile-driven install would accept."
    • Suggested fix: Add a 4th AttemptSpec for ADO AAD bearer (mirroring list_remote_refs lines 943-966), or add a 401-triggered bearer retry in _ref_exists_via_ls_remote's except block when is_ado and dep_auth_scheme=='basic'.
  • [auth-expert] Attempt 1 sends token header over plain HTTP for is_insecure deps at src/apm_cli/deps/github_downloader_validation.py:336

    • Why: is_insecure correctly suppresses credential helpers (attempt 2) and SSH (attempt 3), but attempt 1 is unconditionally built whenever dep_token is set -- including for plain-HTTP repos. The token header injected via build_authorization_header_git_env is then transmitted unencrypted over (redacted)
    • Suggested fix: Guard attempt 1 with if dep_token and not is_insecure: so PAT headers are never injected into plain-HTTP requests.
  • [oss-growth-hacker] Internal module name (github_downloader_validation.py) appears in a user-facing CHANGELOG Fixed entry at CHANGELOG.md

    • Why: CHANGELOG is a conversion surface -- existing users scan it to decide whether to upgrade and share. Naming an internal module is release notes written for maintainers, not users. It breaks the repostable hook and signals complexity where there should be user relief.
    • Suggested fix: Drop the parenthetical about the module entirely ("Validation logic is now a separate module (github_downloader_validation.py), laying groundwork for future credential-provider extensibility."), or move it to a "For contributors" subsection if the project uses one.

Nits (20 items, skip if you want)

  • [python-architect] g.ls_tree('FETCH_HEAD', vpath, env=env) passes auth env to a local command -- env has no effect but creates a false impression credentials are needed at src/apm_cli/deps/github_downloader_validation.py:512
  • [python-architect] Silent early-return for Artifactory in _build_validation_attempts emits no log entry -- verbose probe log will show a gap with no explanation at src/apm_cli/deps/github_downloader_validation.py:326
  • [python-architect] AttemptSpec NamedTuple could be a @dataclass(frozen=True) for field defaults and validators (aesthetic at current size) at src/apm_cli/deps/github_downloader_validation.py:66
  • [cli-logging-expert] Probe hit line [+] {path}@{ref} gives no context about which probe type succeeded (marker-file vs Contents API vs git); scan [+] marker-file: {path}@{ref} style would be more scannable in github_downloader_validation.py
  • [cli-logging-expert] In verbose mode the warn signal (API validation skipped ...) is redundant noise alongside the full probe log; demote to [i] or suppress entirely in verbose mode in install/validation.py
  • [cli-logging-expert] Punctuation inconsistency: all-probes-failed non-verbose suffix uses parentheses (run with --verbose...) while the else branch uses -- run with --verbose... (em-dash); standardise to em-dash across both branches in src/apm_cli/commands/install.py
  • [cli-logging-expert] _warn fallback to _rich_warning when logger is None bypasses the CommandLogger contract; add a comment clarifying logger=None is test-only, or route through a minimal fallback logger in src/apm_cli/install/validation.py
  • [cli-logging-expert] warn_callback message wording leads with mechanism (API validation skipped) not outcome; see devx-ux required finding -- this nit reinforces that required fix in src/apm_cli/install/validation.py
  • [devx-ux-expert] cli-commands.md --verbose description does not mention the new per-probe validation log for subdirectory packages; per persona rule: if a CLI change is not reflected in cli-commands.md in the same PR, the change is incomplete at docs/src/content/docs/reference/cli-commands.md
  • [devx-ux-expert] Audit all warn_callback / _warn call sites for "run with --verbose" suffixes and ensure the same _warn closure strips them uniformly
  • [devx-ux-expert] authentication.md uses "SSO/EMU access" without definition; first-run users won't know what EMU means -- add a parenthetical "(Enterprise Managed Users)" or a link at docs/src/content/docs/getting-started/authentication.md
  • [supply-chain-security-expert] _sanitize_git_error has no catch-all for bare Authorization: Basic/Bearer <value> strings in error text; ADO base64-encoded PATs and AAD JWTs have no prefix pattern and would leak if a git backend echoed the header value to stderr at src/apm_cli/deps/github_downloader.py
  • [supply-chain-security-expert] SHA-pinned ls-remote scan misses non-tip commits (fail-closed, not fail-open, but causes spurious validation failures for valid historic SHA pins); consider a direct shallow fetch of the SHA after ls-remote misses at src/apm_cli/deps/github_downloader_validation.py:435
  • [supply-chain-security-expert] TOCTOU gap between validation and install is inherent but undocumented in the warn_callback message; extend to note "Pin to a commit SHA in apm.lock.yaml for integrity guarantees" at src/apm_cli/deps/github_downloader_validation.py:207
  • [auth-expert] Auth header scheme inconsistency: _directory_exists_at_ref uses Authorization: token {token} while _build_validation_attempts uses Authorization: Bearer {token} for git -- both accepted by GitHub but inconsistent within the module at src/apm_cli/deps/github_downloader_validation.py:291
  • [auth-expert] _allow_fallback gate on SSH ls-remote may surprise HTTPS-only protocol-fallback users who set --allow-protocol-fallback for HTTPS reasons and do not expect an SSH ls-remote attempt at src/apm_cli/deps/github_downloader_validation.py:529
  • [auth-expert] Auth doc mermaid flowchart does not depict the new 4-step validation chain (marker-file -> Contents API -> ls-remote -> shallow-fetch); per persona rules, mermaid must be updated when behaviour changes at docs/src/content/docs/getting-started/authentication.md
  • [oss-growth-hacker] CHANGELOG hook ("works with your existing credential chain") is strong but not repostable as-is -- the SSO/EMU/GHES parenthetical buries the lede; lead with plain-language benefit, then technical proof at CHANGELOG.md
  • [oss-growth-hacker] No quickstart mention of credential-chain transparency; a one-liner under "Authentication" in quick-start.md would close the loop for enterprise users who hit this bug at docs/src/content/docs/getting-started/quick-start.md
  • [oss-growth-hacker] apm-guide skill section describes a four-step probe chain but offers no runnable verification step; note that no --dry-run flag exists yet, or open a follow-up issue and reference it at packages/apm-guide/.apm/skills/apm-usage/authentication.md

CEO arbitration

The panel is unanimous that PR #941 solves a real and painful enterprise problem -- silent auth rejection when an env-var PAT fails but the system credential helper would have succeeded. The two auth-expert required findings, however, reveal that the new validation chain introduces its own correctness gap: it does not mirror the ADO AAD bearer fallback that _clone_with_fallback already performs, meaning a stale ADO PAT will still cause validation to fail even though install would succeed. This directly violates the invariant the PR claims to establish. The plain-HTTP PAT leak is a smaller but unambiguous security regression. Both must be fixed before merge.

The two devx-ux-expert required findings are not cosmetic -- surfacing internal probe names in default error output and emitting a yellow warning on a success path will erode trust in exactly the enterprise audience this fix is trying to win. These findings are consistent with the cli-logging-expert's nits and reinforce each other; the panel is effectively unanimous on the user-facing messaging concerns even where individual severity ratings differ. The oss-growth-hacker's required finding is editorially correct: the CHANGELOG is a conversion surface for evaluators who just hit this bug, and naming the internal module there is release notes written for maintainers.

The architectural extraction from github_downloader.py to github_downloader_validation.py, the AttemptSpec NamedTuple pattern, the winning_attempt threading from _ref_exists_via_ls_remote into _path_exists_in_tree_at_ref, and the validate_path_segments gate at entry are all well-executed. Five targeted fixes -- ADO bearer, plain-HTTP PAT guard, error message wording, warning level/wording, CHANGELOG prose -- will bring this to approval.

Dissent resolved: The cli-logging-expert filed the yellow warning on success path as a nit; devx-ux-expert filed it as required. Siding with devx-ux-expert: a warning-level signal on a success path is a UX correctness issue, not a style preference, because it trains users to ignore warnings and causes panic in a CI log where a yellow signal implies degraded state. The supply-chain-security-expert's TOCTOU + SHA-pin note is correctly filed as a nit -- it is a hardening opportunity, not a regression introduced by this PR.

Growth/positioning note: This fix is a quiet enterprise conversion unlock. The class of user it unblocks -- teams running SSO, EMU, or GHES with system credential helpers -- is exactly the evaluator who hits apm install once, gets a false auth rejection, and silently walks away. Once the required findings are resolved, the release note headline should lead with the plain-language outcome ("apm install now works transparently with your existing GitHub credential chain -- SSO, EMU, and GHES included, no extra config") and bury the probe-chain detail in a contributor changelog subsection. A targeted changelog post to enterprise-on-GitHub channels at release time would convert observers who already encountered this bug.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class GitHubPackageDownloader {
        <<Facade>>
        +validate_virtual_package_exists(dep_ref, verbose_callback, warn_callback) bool
        +_directory_exists_at_ref(dep_ref, path, ref, log) bool
        +_ref_exists_via_ls_remote(dep_ref, ref, log) bool
        +_ssh_attempt_allowed() bool
        +_clone_with_fallback() Repo
        -auth_resolver AuthResolver
    }

    class validate_virtual_package_exists {
        <<IOBoundary>>
        +__call__(downloader, dep_ref, verbose_callback, warn_callback) bool
    }

    class _build_validation_attempts {
        <<Pure>>
        +__call__(downloader, dep_ref, log) list~AttemptSpec~
    }

    class _ref_exists_via_ls_remote {
        <<IOBoundary>>
        +__call__(downloader, dep_ref, ref, log) tuple~bool, AttemptSpec~
    }

    class _path_exists_in_tree_at_ref {
        <<IOBoundary>>
        +__call__(downloader, dep_ref, vpath, ref, log, winning_attempt) bool
    }

    class _directory_exists_at_ref {
        <<IOBoundary>>
        +__call__(downloader, dep_ref, path, ref, log) bool
    }

    class AttemptSpec {
        <<ValueObject>>
        +label str
        +url str
        +env dict
    }

    class DependencyReference {
        <<ValueObject>>
        +repo_url str
        +reference str
        +virtual_path str
        +host str
        +is_virtual bool
        +is_virtual_subdirectory() bool
        +is_azure_devops() bool
    }

    class AuthResolver {
        <<Strategy>>
        +resolve_for_dep(dep_ref) AuthContext
    }

    class GitHubPackageDownloader:::touched
    class validate_virtual_package_exists:::touched
    class _build_validation_attempts:::touched
    class _ref_exists_via_ls_remote:::touched
    class _path_exists_in_tree_at_ref:::touched
    class _directory_exists_at_ref:::touched
    class AttemptSpec:::touched

    classDef touched fill:#fff3b0,stroke:#d47600

    GitHubPackageDownloader ..> validate_virtual_package_exists : delegates (lazy import shim)
    GitHubPackageDownloader o-- AuthResolver : uses
    validate_virtual_package_exists ..> _directory_exists_at_ref : Fallback 1
    validate_virtual_package_exists ..> _ref_exists_via_ls_remote : Fallback 2
    _ref_exists_via_ls_remote ..> _build_validation_attempts : builds chain
    _ref_exists_via_ls_remote ..> AttemptSpec : returns winning
    _path_exists_in_tree_at_ref ..> AttemptSpec : reuses winning
    validate_virtual_package_exists ..> _path_exists_in_tree_at_ref : path confirm
    validate_virtual_package_exists ..> DependencyReference : reads
    _build_validation_attempts ..> AttemptSpec : produces
    _build_validation_attempts ..> AuthResolver : resolves token

    note for _build_validation_attempts "Chain of Responsibility:\ntoken header -> plain HTTPS + cred helper -> SSH\nMirrors _clone_with_fallback auth ordering"
Loading
flowchart TD
    A(["apm install owner/repo/sub#BRANCH"]) --> B["install/validation.py\n_validate_package_exists"]
    B --> C["GitHubPackageDownloader\n.validate_virtual_package_exists (shim)"]
    C --> D["github_downloader_validation\n::validate_virtual_package_exists"]
    D --> E["validate_path_segments(vpath)\n[security gate: blocks .. traversal + empty]"]
    E -->|PathTraversalError| FAIL(["return False"])
    E --> F{"dep_ref type?"}
    F -->|collection| G["[NET] download_raw_file\nmarker: vpath.collection.yml@ref"]
    F -->|file| H["[NET] download_raw_file\nvpath@ref"]
    F -->|subdirectory| I["[NET] download_raw_file\n7 marker paths (apm.yml, SKILL.md, ...)"]
    I -->|any 200| OK(["return True"])
    I -->|all miss| J["[NET] _directory_exists_at_ref\nContents API GET /repos/owner/repo/contents/vpath?ref=ref"]
    J -->|HTTP 200| OK2(["return True"])
    J -->|404 or error| K{"dep_ref.reference truthy?"}
    K -->|no| FAIL2(["return False"])
    K -->|yes| L["[EXEC] _ref_exists_via_ls_remote\n_build_validation_attempts -> AttemptSpec list"]
    L --> M["[EXEC] git ls-remote --heads --tags url ref\nper attempt (token-header HTTPS, plain HTTPS, SSH)"]
    M -->|all fail| FAIL3(["return False, None"])
    M -->|first succeeds| N["winning AttemptSpec returned"]
    N --> O["[FS] tempfile.mkdtemp(prefix=apm-validate-)\ngit init --bare probe.git"]
    O --> P["[EXEC] git fetch --depth=1 --filter=tree:0\norigin ref  (winning_attempt url+env)"]
    P -->|GitCommandError| FAIL4(["return False"])
    P --> Q["[EXEC] git ls-tree FETCH_HEAD vpath"]
    Q -->|empty output| FAIL5(["return False"])
    Q -->|has output| R["[FS] safe_rmtree(tmpdir)"]
    R --> S["warn_callback: 'API validation skipped...\ngit credential fallback'"]
    S --> OK3(["return True"])
    G -->|200| OK
    H -->|200| OK
Loading

Design patterns

  • Used in this PR: Chain of Responsibility -- _build_validation_attempts assembles an ordered AttemptSpec list (token-header HTTPS -> plain HTTPS+cred-helper -> SSH) mirroring _clone_with_fallback's auth ordering; each attempt is tried in turn until one succeeds.
  • Used in this PR: ValueObject (NamedTuple) -- AttemptSpec is an immutable, typed 3-tuple carrying (label, url, env) through the chain. Its key contribution is that the winning instance is threaded from _ref_exists_via_ls_remote into _path_exists_in_tree_at_ref, guaranteeing the same credential that proved the ref exists is the one used for the follow-up fetch.
  • Pragmatic suggestion: none -- the module-level-function-with-downloader-as-first-arg style is a reasonable procedural extraction for a 2400-line-cap enforcement. Introducing a ValidationStrategy class hierarchy would add abstraction without a third call site to justify it.

Required: None.
Nits: See aggregated list above (auth env on ls-tree, Artifactory silent skip, AttemptSpec dataclass suggestion).

CLI Logging Expert

No required findings.
Nits: See aggregated list above (probe type context on hit line, warn_callback wording, verbose-mode warn demotion, punctuation consistency, _rich_warning bypass comment).

DevX UX Expert

Required: 2 (see aggregated required above).
Nits: See aggregated list above (cli-commands.md verbose description, suffix stripping audit, EMU definition).

Supply Chain Security Expert

No required findings.
Nits: See aggregated list above (_sanitize_git_error Authorization header catch-all, SHA-pinned ls-remote non-tip miss, TOCTOU warn_callback message).

Auth Expert

Required: 2 (see aggregated required above).
Nits: See aggregated list above (token vs Bearer scheme consistency, _allow_fallback SSH surprise, mermaid flowchart not updated).

OSS Growth Hacker

Required: 1 (see aggregated required above).
Nits: See aggregated list above (CHANGELOG lede, quickstart credential-chain mention, skill section runnable example).

Verdict computed deterministically: 5 required findings across 3 active panelists (devx-ux-expert: 2, auth-expert: 2, oss-growth-hacker: 1). APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Generated by PR Review Panel for issue #941 · ● 4.1M ·

@github-actions github-actions Bot added the panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. label Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants