Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .apm/architecture/owners/transport-auth-platform.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"version": 1,
"owners": [
{
"id": "git-transport-selection",
"decision": "Initial Git scheme, ordered protocol attempts, and GitLab sparse-fetch plan consumption",
"owner": "deps/transport_selection.py (initial_transport_scheme, TransportSelector); deps/download_strategies.py (DownloadDelegate.download_gitlab_file)",
"selectors": [
"src/apm_cli/deps/transport_selection.py",
"src/apm_cli/deps/download_strategies.py"
],
"guards": ["transport-platform-gitlab-sparse-plan"]
},
{
"id": "unix-install-ownership",
"decision": "Unix installer destinations, prior-install ownership, native shell setup receipt and unprivileged replacement",
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The shared gh-aw APM pack job now declares `contents: read` (previously `permissions: {}`), the minimum the explicit built-in-token path needs. No write scope is added, and the token is not forwarded to restore or agent jobs. (#2706)
- Dependency policy `allow`, `deny`, and exact `require` matching now follows canonical owner/repository casing, fixing mixed-case blocks and deny fail-open behavior while retaining lazy shared required-package lookup. APM 0.30.0 and earlier match patterns byte-exactly against the lowercased identity; lowercase patterns keep matching in every release, so drop workaround duplicates only after every runner uses a release carrying this fix. (#2706)

### Fixed

- GitLab `path:` dependencies now preserve the selected SSH transport, username, and port instead of silently using HTTPS; REST fallback requires an executed same-origin HTTPS attempt admitted by the transport policy. (#2938)

## [0.30.0] - 2026-09-07

### Security
Expand Down
10 changes: 8 additions & 2 deletions CONFORMANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1056,8 +1056,11 @@
"keyword": "MUST",
"section": "7.2",
"status": "active",
"test_count": 7,
"test_count": 10,
"tests": [
"tests/spec_conformance/test_gitlab_sparse_transport_reqs.py::test_gitlab_sparse_fetch_preserves_port_identity_and_ref[main]",
"tests/spec_conformance/test_gitlab_sparse_transport_reqs.py::test_gitlab_sparse_fetch_preserves_port_identity_and_ref[sha]",
"tests/spec_conformance/test_gitlab_sparse_transport_reqs.py::test_gitlab_sparse_fetch_preserves_port_identity_and_ref[v1]",
"tests/spec_conformance/test_resolution_reqs.py::test_repository_identity_isolates_l0_cache_across_hosts",
"tests/spec_conformance/test_resolution_reqs.py::test_repository_identity_isolates_same_host_nested_repositories_through_resolver",
"tests/spec_conformance/test_resolution_reqs.py::test_repository_identity_normalizes_safe_syntax_dimensions",
Expand Down Expand Up @@ -1231,8 +1234,11 @@
"keyword": "MUST",
"section": "10.3",
"status": "active",
"test_count": 1,
"test_count": 4,
"tests": [
"tests/spec_conformance/test_gitlab_sparse_transport_reqs.py::test_gitlab_sparse_fetch_preserves_port_identity_and_ref[main]",
"tests/spec_conformance/test_gitlab_sparse_transport_reqs.py::test_gitlab_sparse_fetch_preserves_port_identity_and_ref[sha]",
"tests/spec_conformance/test_gitlab_sparse_transport_reqs.py::test_gitlab_sparse_fetch_preserves_port_identity_and_ref[v1]",
"tests/spec_conformance/test_manifest_reqs.py::test_configured_host_class_precedence_is_credential_isolated"
]
},
Expand Down
4 changes: 2 additions & 2 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Repository-coordinate segments are case-insensitive for `github.com`, GitHub Ent
| [req-rs-013](docs/src/content/docs/specs/openapm-v0.1.md#req-rs-013) | MUST | 7.2 | consumer | active | 1 | - |
| [req-rs-014](docs/src/content/docs/specs/openapm-v0.1.md#req-rs-014) | MUST | 7.3.1 | consumer | active | 1 | - |
| [req-rs-015](docs/src/content/docs/specs/openapm-v0.1.md#req-rs-015) | MUST | 7.5 | consumer | active | 1 | - |
| [req-rs-016](docs/src/content/docs/specs/openapm-v0.1.md#req-rs-016) | MUST | 7.2 | consumer | active | 7 | - |
| [req-rs-016](docs/src/content/docs/specs/openapm-v0.1.md#req-rs-016) | MUST | 7.2 | consumer | active | 10 | - |
| [req-rs-017](docs/src/content/docs/specs/openapm-v0.1.md#req-rs-017) | MUST | 7.7 | consumer | active | 15 | - |
| [req-sc-001](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-001) | MUST | 10.4 | consumer | active | 2 | - |
| [req-sc-002](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-002) | MUST | 10.9 | consumer | active | 1 | - |
Expand All @@ -142,7 +142,7 @@ Repository-coordinate segments are case-insensitive for `github.com`, GitHub Ent
| [req-sc-010](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-010) | MUST | 10.13 | consumer | active | 1 | - |
| [req-sc-011](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-011) | MUST | 10.14 | consumer | active | 1 | - |
| [req-sc-012](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-012) | MUST | 10.14 | consumer | active | 1 | - |
| [req-sc-013](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-013) | MUST | 10.3 | consumer | active | 1 | - |
| [req-sc-013](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-013) | MUST | 10.3 | consumer | active | 4 | - |
| [req-sc-014](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-014) | MUST | 10.15 | consumer | active | 1 | - |
| [req-sc-015](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-015) | MUST | 10.16 | consumer | active | 3 | tests/fixtures/spec-conformance/source-plan/req-sc-015.json |
| [req-tg-001](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-001) | MUST | 8.4 | consumer | active | 1 | - |
Expand Down
38 changes: 21 additions & 17 deletions docs/src/content/docs/consumer/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,33 @@ key non-interactively or use token-backed HTTPS.

## GitLab (SaaS or self-managed)

**If `git clone` works, `apm install` works** -- no token is needed for GitLab `path:` files.

APM fetches `path:`-specified files from GitLab dependencies via git
sparse/partial checkout (the same transport used for the clone), so your
existing SSH keys and git credential helpers work without any extra token.
This is the default for all GitLab sources, including self-hosted instances
where the REST API is restricted or returns 410 -- if `git clone` works, so
does `apm install`. For self-hosted hosts, explicit `git:` / SSH URLs carry
the host in the dependency. Set `GITLAB_HOST` (or `APM_GITLAB_HOSTS`) only
when you want bare-host or shorthand forms to classify as GitLab.
If you need to fall back to the GitLab REST API (for environments where git
transport is not available), set `GITLAB_APM_PAT`:
GitLab `path:` single-file fetches use sparse/partial Git checkout and the
same [transport policy](../manage-dependencies/#transport-selection) as clones.
SSH keys and Git credential helpers work without an extra token, even when
the REST API is disabled.

In strict mode, explicit SSH/SCP URLs and SSH preference keep these fetches
on SSH, preserving the user, host, port, and ref. Git failure never triggers
REST, even with a PAT available. Fix SSH access or explicitly declare the
HTTPS web endpoint; APM does not map SSH aliases to web hostnames.

REST fallback runs only after the selected Git plan is exhausted and an
executed attempt used effective HTTPS with the same normalized scheme,
host, and port as the API endpoint. Default HTTPS fallback remains supported.
HTTP is not automatically upgraded to HTTPS; an HTTPS URL rewritten by Git
to SSH or a local mirror does not authorize REST.

For token-backed HTTPS:

```bash
export GITLAB_APM_PAT=glpat_your_token
apm install
```

Use a project- or group-scoped token with **read_repository** scope. Self-managed GitLab works with the same env var; APM resolves the host from the dependency URL.

If you have configured a git credential helper for GitLab (e.g. `git credential-manager` on Windows / macOS), APM falls back to it after the env-var lookup -- you do not need `GITLAB_APM_PAT` if `git clone https://gitlab.com/<your-group>/<repo>` already prompts you once and caches.

`GITLAB_TOKEN` is also accepted as a lower-precedence fallback for compatibility with CI environments that already set it.
Use **read_repository** scope. APM checks `GITLAB_APM_PAT`, then
`GITLAB_TOKEN`, then the Git credential helper for trusted GitLab hosts.
For self-managed host registration and token trust, see
[GitLab authentication](../../getting-started/authentication/#gitlab-saas-and-self-managed).

## Azure DevOps

Expand Down
18 changes: 12 additions & 6 deletions docs/src/content/docs/consumer/manage-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,8 @@ the HTTP file-read path. If a private host fails with 401/403, use a whole-repo
git dependency for full clone auth support, or choose the supported HTTP backend
signal (`type: gitlab` for GitLab-compatible hosts, `GITHUB_HOST` for GHES).

**GitLab `path:` fetch transport:** GitLab `path:` files are fetched over
git transport (not the REST API), so self-hosted instances with the API disabled
still install. See [Authentication](../authentication/#gitlab-saas-or-self-managed).
**GitLab `path:` files** use Git first, with
[restricted REST fallback](../authentication/#gitlab-saas-or-self-managed).

For private repos and non-GitHub hosts, see
[Private and org packages](../private-and-org-packages/).
Expand Down Expand Up @@ -209,12 +208,13 @@ Git-hook isolation guarantee.

## Transport selection

APM selects one initial transport per dependency. Git then applies any matching
safe `url.<base>.insteadOf` rule to that selected URL.
APM selects one initial transport per dependency, including GitLab `path:`
single-file sparse fetches. Git then applies matching safe
`url.<base>.insteadOf` rules.

| Dependency form | Initial transport |
|---|---|
| `ssh://...` or `git@host:...` | SSH |
| `ssh://...` or `user@host:...` (SCP-style) | SSH |
| `https://...` or `http://...` | The explicit HTTP(S) scheme |
| Shorthand with `--ssh`, `APM_GIT_PROTOCOL=ssh`, or saved `prefer-ssh` | SSH |
| Other shorthand | HTTPS |
Expand All @@ -232,6 +232,12 @@ preference with `apm config set prefer-ssh true`, or save the retry escape hatch
with `apm config set allow-protocol-fallback true`. See the
[`apm config` reference](../../reference/cli/config/).

Opt-in SSH/HTTPS fallback reuses the declared custom port and warns; it does
not map an SSH alias to a web hostname. If protocols use different endpoints,
declare the intended URL instead. GitLab REST additionally requires an
executed same-origin HTTPS attempt; see
[GitLab authentication](../authentication/#gitlab-saas-or-self-managed).

If Git reports an HTTPS `Failed to connect...` / `Couldn't connect to server`
error for the requested remote, APM retries that Git action once after 1 second
with the same URL, credentials, and transport. A persistent connection failure
Expand Down
33 changes: 15 additions & 18 deletions packages/apm-guide/.apm/skills/apm-usage/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,23 +214,20 @@ apm pack # marketplace.json also resolves agains

## GitLab (SaaS or self-managed)

APM fetches `path:`-specified files from GitLab dependencies via git sparse/partial
checkout (the same transport as the clone). Git transport is tried first, so SSH
keys and git credential helpers work without any extra token, and self-hosted
GitLab instances where the API returns 410 (disabled) no longer fail. Explicit
`git:` / SSH URLs carry the host in the dependency; set `GITLAB_HOST` (or
`APM_GITLAB_HOSTS`) only when bare-host or shorthand forms should classify as
GitLab.

If git transport is unavailable, `GITLAB_APM_PAT` is the fallback:

```bash
export GITLAB_APM_PAT=glpat_your_token
apm install
```

`GITLAB_TOKEN` is accepted as a lower-precedence fallback. `git credential fill` is
also tried (same as for GitHub) so credential-manager users need no env var at all.
GitLab `path:` single-file sparse fetches follow the clone transport policy:
`--ssh`, `APM_GIT_PROTOCOL`, saved `prefer-ssh`, and opt-in
`--allow-protocol-fallback` / `APM_ALLOW_PROTOCOL_FALLBACK`. Strict SSH/SCP
and SSH preference preserve user, host, port, and ref; Git failure never
unlocks REST merely because a PAT exists. Fix SSH or declare the HTTPS web
endpoint.

Default HTTPS compatibility remains. REST requires an exhausted Git plan
and an executed effective HTTPS attempt matching the API's normalized
scheme/host/port. HTTP is not upgraded; HTTPS rewritten to SSH/local does
not qualify. Opt-in alternate protocol reuses the declared custom port and
warns, without mapping SSH aliases to web hostnames. See the
[GitLab fetch policy](https://microsoft.github.io/apm/consumer/authentication/#gitlab-saas-or-self-managed)
and [GitLab hosts](#gitlab-hosts) for token trust.

## GHE Cloud data residency (*.ghe.com)

Expand Down Expand Up @@ -406,7 +403,7 @@ credential under a fully qualified `https://<host>:<port>/` URL.

### SSH connection hangs on corporate/VPN networks

APM tries SSH as a fallback when HTTPS auth is not available. It forces
APM tries SSH when selected or cross-protocol fallback is enabled. It forces
`BatchMode=yes`, disables askpass and HTTP credential channels, and uses a
30-second connection timeout so SSH attempts fail without prompting.

Expand Down
18 changes: 4 additions & 14 deletions scripts/architecture_linter/checks/transport_auth_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
direct_definitions,
effective_definition,
)
from scripts.architecture_linter.checks.transport_gitlab_sparse import (
check_gitlab_prepared_remote,
)
from scripts.architecture_linter.checks.transport_platform_shared import (
_SRC_PREFIX,
GROUP,
Expand Down Expand Up @@ -1031,20 +1034,7 @@ def _check_git_child_environment(provider: FactsProvider) -> tuple[Violation, ..
exempt=False,
)
)
findings.extend(
_require_subs(
provider,
inv,
_RID_GIT_CHILD_ENV,
"src/apm_cli/deps/download_strategies.py",
(
"tokenless_url_builder = partial(",
'token="",',
"build_repo_url_fn=tokenless_url_builder",
),
"Git file transport must keep managed credentials out of remote URLs",
)
)
findings.extend(check_gitlab_prepared_remote(provider, _RID_GIT_CHILD_ENV))
findings.extend(
_require_subs(
provider,
Expand Down
Loading