fix(auth): thread dep_ref.port into credential resolution (#785)#788
Conversation
) DependencyReference.port was parsed and stored but never reached the authentication path, so same-host multi-port setups (e.g. Bitbucket Datacenter with distinct PATs on 7990 and 7991) could collapse onto a single credential entry via the AuthResolver cache. - HostInfo gains a port field and a display_name property for user-facing identifiers; classify_host stays transport-agnostic -- port does not influence host kind. - AuthResolver._cache key widens to (host, port, org). port=None keeps the pre-fix key for the >99% common case; custom-port setups get per-port entries. - TokenManager._credential_cache mirrors the widening, keyed by (host, port). A half-widened cache would return the wrong cached credential at the collapsed layer. - git credential fill sends host=host:port per gitcredentials(7). The credential protocol has no standalone port= attribute; a separate port= line is silently dropped by every helper. - resolve_for_dep threads dep_ref.port; downloader, validation, and sources call sites forward the port argument. - build_error_context surfaces host:port in the failure summary and appends an [i] hint when a port is set, pointing users at credential helpers that key by hostname only. Lockfile identity is unaffected -- DependencyReference.get_unique_key() still excludes port by design; the widened keys are purely in-process memoisation. Refs: microsoft#661, microsoft#665, microsoft#784
|
@danielmeppiel — ready for review when you have a cycle. Scope is the 9-item PR spec from your review-panel comment on #785 (standalone PR, both caches widened, |
APM Review Panel — PR #788[+] Verdict: APPROVE — merge as-is, file two follow-ups @edenfunf delivered a faithful 9-for-9 execution on the panel ask from #785. Four specialists (python-architect, auth-expert, cli-logging-expert, devx-ux-expert) converge on mergeable. The only "blocker" surfaced (architect flag on What's excellent
Findings
Merge callShip it. Open follow-ups for #2, #3, #4. @edenfunf — this is the standard for contributor work on a multi-part ask: read the full thread, execute every item, write the tests that prove the contract. Thank you for the precision. [+] Growth angle
Reviewed via the |
There was a problem hiding this comment.
Pull request overview
Threads DependencyReference.port through APM’s auth/credential resolution so same-host multi-port setups don’t collide in in-process caches or git credential fill lookups.
Changes:
- Add
porttoHostInfo, widenAuthResolvercache keys, and propagateportthrough resolve/fallback/error-context paths. - Embed port into
git credential fillashost=host:portand segregateGitHubTokenManagercredential cache by(host, port). - Add/adjust unit tests, update auth docs, and add a changelog entry.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/core/auth.py |
Adds port-aware HostInfo, port-threading through auth resolution, cache key widening, and port-aware error messaging. |
src/apm_cli/core/token_manager.py |
Embeds port into git-credential host field and keys credential cache by (host, port). |
src/apm_cli/deps/github_downloader.py |
Threads dep_ref.port into auth error-context generation and GitHub file auth resolution. |
src/apm_cli/install/validation.py |
Threads dep_ref.port into auth classification, resolution, fallback, and verbose diagnostics. |
src/apm_cli/install/sources.py |
Threads dep_ref.port into auth resolution used for package auth logging. |
tests/unit/test_auth.py |
Adds coverage for port propagation, cache discrimination, error context display/hints, and fallback behavior. |
tests/test_token_manager.py |
Updates existing assertions and adds tests for (host, port) credential cache + credential-fill stdin format. |
tests/test_github_downloader.py |
Adjusts mocks for new resolve_credential_from_git(host, port=...) signature. |
docs/src/content/docs/getting-started/authentication.md |
Documents port-in-host behavior and helper port-awareness caveats. |
CHANGELOG.md |
Adds an Unreleased “Fixed” entry for port-discriminated token resolution. |
Copilot's findings
- Files reviewed: 10/10 changed files
- Comments generated: 5
|
@edenfunf check Copilot Review and CodeQL scan alert and I will approve after that's done, thanks! |
- _try_credential_fallback now reads host_info.host / host_info.port instead of the closure host / port variables. Behaviourally identical today (the resolver always passes the same host into both) but matches the symmetry of _resolve_token at the same indirection layer and removes a future drift hazard if try_with_fallback ever takes a pre-resolved AuthContext directly. - HostInfo.display_name and _format_credential_host now use ``port is not None`` instead of truthy checks. ``None`` is the sentinel for "no port" everywhere else in the resolver (build_error_context already used this form), so the truthy fallback was the only inconsistency. Functionally equivalent for valid TCP ports; tightens the contract against any future port=0 misuse. - Mirror the new "Custom-port hosts and per-port credentials" section from docs/getting-started/authentication.md into the in-repo APM usage skill at packages/apm-guide/.apm/skills/apm-usage/, so the skill APM ships with stays consistent with the public docs. - Anchor the build_error_context substring assertion in test_auth.py with the surrounding " on " / "." tokens. The previous "<host:port>" in <message> form was flagged by CodeQL's py/incomplete-url-substring-sanitization rule (false positive in test context, but the anchored form is also strictly more precise -- it pins the rendered position rather than just substring existence). Default-port normalisation (port=443/80/22 still rendering host:port) is intentionally left for a follow-up issue per the panel review -- that fix belongs at the parser or HostInfo.__post_init__ layer and needs its own contract test.
|
@danielmeppiel — pushed In this push
Replies to remaining review threads
Verification
|
…ination # Conflicts: # CHANGELOG.md
danielmeppiel
left a comment
There was a problem hiding this comment.
APM Review Panel -- PR #788 (re-review)
[+] APPROVE. All in-PR items from my prior approve-with-fixes are addressed in f5fdb07; deferred items have follow-up issues.
Verified in f5fdb07
- dani #1 (
auth.py:309):_try_credential_fallbacknow readshost_info.host/host_info.port, matching_resolve_token's symmetry. Drift hazard removed. - CodeQL high alert:
build_error_contextsubstring assertion anchored with" on "/"."tokens. Strictly more precise than the bare form, beyond just silencing the false positive. - Copilot #2/#3:
HostInfo.display_nameand_format_credential_hostswitched toport is not None-- matches the sentinel convention used everywhere else and tightens the contract against any futureport=0misuse. - Copilot #4:
packages/apm-guide/.apm/skills/apm-usage/authentication.mdmirrored with the new "Custom-port hosts and per-port credentials" section, keeping the in-repo APM usage skill in sync with public docs (per the doc-sync rule incli.instructions.md).
Follow-ups filed
- #797 -- default-port (443/80/22) normalization in parser/
HostInfo.__post_init__+ contract test (dani #2). - Pending issues for
is_genericerror path port loss (dani #3) and[i]hint actionability snippet (dani #4) -- batched for a docs polish pass.
Accepted as-is
- Copilot #1 (cache-key
org=None->""): matches panel spec verbatim; no caller passesorg="". Behaviourally identical, internal to the resolver.
Nice work, @edenfunf -- this is the cleanest 9-for-9 panel execution we've seen, with contract tests and an anti-pattern guard for gitcredentials(7). Thanks for the precision.
Re-reviewed via apm-review-panel skill: prior approve-with-fixes items verified individually against f5fdb07 diff.
- Bump version to 0.9.0 in pyproject.toml and uv.lock - Move CHANGELOG [Unreleased] entries into [0.9.0] - 2026-04-21 - Add fresh empty [Unreleased] header - Consolidate the two scattered ### Changed subsections into one - Backfill missing entries: build-time self-update policy (#675), APM Review Panel skill instrumentation (#777) - Backfill PR refs on a few entries that referenced issue numbers - Promote the previously-orphaned 'apm install --mcp' / VS Code adapter / init Next Steps lines to consistent (#PR) attribution - Remove stray blank line inside the ### Added block - Credit external contributors inline (@arika0093 #700, @joostsijm #675, @edenfunf #788) Highlights of 0.9.0: BREAKING CHANGES - MCP entry validation hardened (#807) - Strict-by-default transport selection (#778) - Whitespace stdio MCP commands now rejected at parse time (#809) ADDED - apm install --mcp for declarative MCP server addition (#810) - --registry flag and MCP_REGISTRY_URL for custom MCP registries (#810) - HTTP-dependency support via --allow-insecure dual opt-in (#700) - apm install --ssh / --https flags for transport selection (#778) - Multi-target apm.yml + --target flag (#628) - Marketplace UX: view, outdated, validate (#514) - Build-time self-update policy for package-manager distros (#675) - APM Review Panel + 4 specialist personas (#777) This PR is release machinery and is intentionally excluded from the [0.9.0] section per .github/instructions/changelog.instructions.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Closes #785. Follow-up from the #665 review panel's auth-expert finding and the design discussion on the issue thread.
DependencyReference.portwas parsed and stored by #665 but never reached the authentication path. Same-host multi-port setups (e.g. Bitbucket Datacenter with distinct PATs on 7990 and 7991) therefore collapsed onto a single credential entry via theAuthResolvercache, causing APM's pre-resolved token path (Method 1: authenticated HTTPS) to return the wrong credential.The fix threads
portthrough every layer that identifies a credential target:HostInfo, both in-process caches,git credential fillstdin, and user-facing error output.What changed
HostInfogains aport: Optional[int] = Nonefield and adisplay_nameproperty ("host:port" if port else host). Classification stays transport-agnostic --classify_hostcarries the port through but never uses it to decidekind.AuthResolver._cachekey widens to(host.lower(), port, org.lower() if org else "").port=Nonepreserves the pre-fix key for the >99% common case; multi-port setups get per-port entries. Lock semantics unchanged.TokenManager._credential_cacheis keyed by(host, port)tuple. Both caches widen together -- a half-widened cache would return the wrong cached credential at the collapsed layer.git credential fillreceiveshost=host:portpergitcredentials(7). The credential protocol has no standaloneport=attribute; sending one would be silently ignored by every helper. The_format_credential_hosthelper and an anti-pattern test guard against this regression.resolve_for_depthreadsdep_ref.port;github_downloader,validation, andsourcescall sites forward the port argument.build_error_contextuseshost_info.display_namein the failure summary and, when a port is set, appends one[i]hint pointing users at credential helpers that key by hostname only.docs/getting-started/authentication.mdgains a per-helper port-awareness table under the "Git credential helper not found" section.[Unreleased]/Fixed.Lockfile identity is unaffected --
DependencyReference.get_unique_key()still excludes port by design; the widened cache keys are purely in-process memoisation.Type of change
Testing
New test coverage (23 cases)
TestHostInfoPort-- field,display_name,classify_hostport attachment + transport-agnostic invariant.TestResolvePortDiscrimination-- same host / different port → separate entries; same(host, port, org)hits cache;port=Nonevsport=<int>are distinct;resolve_for_depthreads port end-to-end for bothssh://andhttps://URLs;HostInfocarries port on resolve.TestBuildErrorContextWithPort--display_namein error;[i]hint appears when port is set; no hint when port is absent.TestTryWithFallbackWithPort-- port flows into the credential-fill fallback path.TestCredentialFillPortEmbedding--host=host:portstdin format; no standaloneport=line (anti-pattern guard); backward-compat with bare host whenport=None.TestGetTokenWithCredentialFallbackgains same-host-different-port + same-host-same-port cache tests.Manual end-to-end verification
Ran a scripted check covering: ssh/https port capture,
resolve_for_deppropagation, cache-hit behaviour, git credential fill stdin format,TokenManagercache segmentation, and error-context hint presence/absence. All six scenarios pass.Notes for reviewers
port=line ingit credential fillstdin) is not implemented -- the port is embedded into thehostfield instead, pergitcredentials(7). An anti-pattern regression test asserts no\nport=ever appears in the stdin.classify_hostacceptsportas a kwarg and attaches it to the returnedHostInfo. Classification logic itself remains transport-agnostic as per the architect's note; the kwarg is purely metadata.policy/discovery.py,marketplace/client.py,registry_proxy.py) are intentionally left alone -- they either do not parse adep_ref(policy discovery) or only usehost_info.kind/api_basefor classification, where port is irrelevant.Refs: #661, #665, #784