Skip to content

fix(deps): allow same-repo remote path deps#1732

Merged
danielmeppiel merged 4 commits into
mainfrom
danielmeppiel/1571-local-deps-shared-repo
Jun 11, 2026
Merged

fix(deps): allow same-repo remote path deps#1732
danielmeppiel merged 4 commits into
mainfrom
danielmeppiel/1571-local-deps-shared-repo

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

fix(deps): allow same-repo remote path deps

TL;DR

Remote packages can now declare a relative path: dependency on a sibling package in the same remote repository. The resolver expands the sibling to the parent's remote host/repo/ref and keeps the existing fail-closed guard for absolute paths, repo-root escapes, and cross-repo local paths. Closes #1571.

Important

This is intentionally scoped to same-repo sibling path: resolution only. It is not general workspace semantics, namespaces, or cross-repo path dependency support.

Problem (WHY)

  • Issue [BUG] or [QUESTION] Local dependencies in shared repository #1571 reports the current guardrail blocks monorepo package layouts with an error: Refusing to install local_path dependency './_shared' declared by remote package 'development-backend-dotnet'.
  • The requested behavior is same-repo sibling resolution: I would expect the _shared/ apm package to be installed as well from that same remote repository.
  • The implementation stays narrow and test-backed because the workflow guidance is to Add what the agent lacks, omit what it knows and because validation should be deterministic: Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action.

Approach (WHAT)

Area Decision
Allowed case A remote package's relative path: may resolve to a sibling only if the final path stays inside the same cloned repo root.
Fetch behavior Convert the local path into a remote virtual dependency using the parent's host, repo, and ref, so downloader/cache code reuses the same origin.
Rejected cases Absolute paths, repo-root escapes, registry parents, and sibling paths that point into another repo clone still fail closed.
Docs Update dependency docs and usage skill notes so the new boundary is visible to users and agents.

Implementation (HOW)

File Change
src/apm_cli/deps/apm_resolver.py Adds same-repo expansion for remote-parent path: deps, computes the parent repo root from source_path + virtual_path, validates containment through path_security, and preserves the fallback rejection set.
tests/test_apm_resolver.py Adds regression coverage for same-repo sibling resolution plus escaping and cross-origin rejection.
src/apm_cli/deps/path_anchoring.py, src/apm_cli/install/phases/local_content.py Updates comments/docstrings so the local-path security model matches the new resolver boundary.
Docs and guide files Document that remote path: deps are same-repo only and still reject unsafe paths.
CHANGELOG.md Adds the user-visible fix under [Unreleased].

Diagram

The diagram shows the new resolver fork: eligible sibling paths become same-origin remote virtual deps; unsafe paths stay rejected.

sequenceDiagram
    participant P as Remote parent apm.yml
    participant R as Resolver
    participant C as Parent clone
    participant D as Downloader
    P->>R: path: ../shared
    R->>C: Resolve against parent source path
    alt Inside same repo root
        R->>D: Expand to virtual path packages/shared
        D-->>R: Fetch sibling from same host, repo, and ref
    else Outside repo root
        R-->>P: Reject and mark failed
    end
Loading

Trade-offs

  • Keeps path: object form as the user-facing syntax rather than adding a new manifest construct.
  • Expands to remote virtual dependencies instead of copying from apm_modules, avoiding consumer-filesystem confusion and preserving existing downloader auth/cache behavior.
  • Does not solve broader workspace or cross-repo path semantics; those remain out of scope for this PR.
  • Maintains the existing rejection tracking path so integration still skips dependencies that failed during resolution.

Benefits

  1. Remote monorepos can co-develop sibling APM packages on the same branch/ref without hard-coded full repo URLs.
  2. The anti-exfiltration boundary is preserved: only paths under the authenticated repo root are eligible.
  3. Existing ADO/GitLab/GitHub download paths remain the source of truth because expansion preserves the parent's remote coordinates.
  4. Regression tests cover the success path and both security fences requested by the board.

Validation

Scenario Evidence

Scenario APM promise Proof
Remote packages/frontend declares path: ../shared in the same repo Consume tests/test_apm_resolver.py::TestRemoteParentLocalPathFailClosed::test_remote_parent_same_repo_sibling_path_expands_to_remote_virtual_dep
Remote package tries to escape the repo root Govern tests/test_apm_resolver.py::TestRemoteParentLocalPathFailClosed::test_remote_parent_path_escape_outside_repo_root_is_rejected
Remote package tries to point into a sibling repo clone Govern tests/test_apm_resolver.py::TestRemoteParentLocalPathFailClosed::test_remote_parent_path_to_different_repo_clone_is_rejected
Commands run
uv run --extra dev pytest tests/test_apm_resolver.py tests/unit/test_git_parent_reference.py tests/unit/install/test_resolve_resolution_paths.py tests/unit/test_lockfile_git_parent_expanded.py tests/unit/test_lockfile_v2_bump.py -q
108 passed in 1.15s

uv run --extra dev ruff check src/ tests/
All checks passed!

uv run --extra dev ruff format --check src/ tests/
1237 files already formatted

uv run --extra dev python <python equivalents for YAML I/O, file-length, and raw str(relative_to) guards>
# exit 0

uv run --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/
Your code has been rated at 10.00/10

bash scripts/lint-auth-signals.sh
[+] auth-signal lint clean

How to test

  • Create a remote monorepo package where packages/frontend/apm.yml declares - path: ../shared.
  • Run apm install <same-repo>/packages/frontend#<branch>.
  • Confirm APM installs both frontend and shared from the same branch/ref.
  • Change the dependency to an escaping path such as ../../../outside and confirm install rejects it.
  • Change the dependency to a sibling repo path such as ../other-repo/shared and confirm install rejects it.

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

danielmeppiel and others added 2 commits June 11, 2026 00:11
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cal-deps-shared-repo

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 10, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Enables a narrowly-scoped behavior change in the dependency resolver: remote (git-cloned) packages may now use relative path: dependencies to reference sibling packages within the same remote repository, while continuing to reject absolute paths, repo-escape traversal, and cross-repo paths.

Changes:

  • Add resolver logic to expand eligible remote-parent relative path: dependencies into same-origin remote virtual dependencies.
  • Add regression tests covering the allowed same-repo sibling case and key fail-closed rejection cases.
  • Update inline docs/comments and user-facing docs to describe the new same-repo-only boundary, plus a changelog entry under [Unreleased].
Show a summary per file
File Description
src/apm_cli/deps/apm_resolver.py Implements same-repo expansion + centralized rejection reporting for remote-parent path: deps.
tests/test_apm_resolver.py Adds coverage for sibling expansion and fail-closed path rejection scenarios.
src/apm_cli/install/phases/local_content.py Updates docstring commentary to reflect resolver-level expansion/rejection boundary.
src/apm_cli/deps/path_anchoring.py Updates module-level commentary on the untrusted-source boundary (remote-parent path handling).
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Documents same-repo-only semantics for remote-declared relative path: deps.
docs/src/content/docs/reference/manifest-schema.md Documents remote path: scoping to the same repo root and rejection cases.
docs/src/content/docs/consumer/manage-dependencies.md Adds an example + narrative describing remote monorepo sibling path: usage and constraints.
CHANGELOG.md Adds an [Unreleased] fixed entry for #1571 behavior change.

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 2

Comment on lines +357 to +360
local_path = Path(local_str.replace("\\", "/"))
resolved = ensure_path_within(parent_source / local_path, repo_root)
virtual_path = portable_relpath(resolved, repo_root)
if virtual_path in ("", "."):
Comment thread CHANGELOG.md Outdated
Comment on lines +21 to +23
### Fixed

- `apm install` now resolves relative `path:` deps declared by remote monorepo packages when they stay inside the same remote repo, while still rejecting absolute, escaping, or cross-repo paths. (closes #1571)
danielmeppiel and others added 2 commits June 11, 2026 01:12
Folds copilot-pull-request-reviewer follow-ups: derive contained sibling relpath directly (avoid Windows absolute-path footgun #886), changelog PR number.

Refs #1571

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keeps the closes #1571 note in the entry sentence while ending the changelog line with the PR number.

Refs #1571

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

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

Lets a remote monorepo package resolve a same-repo sibling path: dep by reusing the existing clone, while keeping absolute, repo-escaping, and cross-repo paths fail-closed. Implements #1571.

cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

This PR closes the gap that #1571 flagged: remote monorepos can now co-develop sibling APM packages on one branch without hard-coding full repo URLs. The design is the conservative one -- a relative path: declared by a remote parent is expanded into a same-origin virtual dependency (parent host/repo/ref inherited, registry_name reset), so the existing authenticated clone and cache are reused rather than a new origin or credential being introduced. The security boundary runs through the sanctioned path_security guards: ensure_path_within resolves symlinks and asserts containment against the computed repo root, validate_path_segments rejects traversal, and registry/absolute parents are refused before any disk touch.

Because this surface is shared with #1014 (PR #1740 is stacked on this branch), the panel weighted the supply-chain-security and auth lenses heavily. Both converge: the containment fence is correct and fail-closed, and the auth model is the safe one (no second credential path, no origin confusion). No panelist raised a blocking-severity finding. The recommended items are test-hardening and wording polish that the maintainer can fold now or track post-merge -- none of them gate this change.

Aligned with: secure by default (symlink-resolving containment, registry/absolute/cross-repo rejected, fail-closed fallback), pragmatic as npm (monorepo sibling parity with npm/pip workspace resolution), portable by manifest (no new manifest construct -- path: object form preserved), multi-harness multi-host (expansion preserves the parent's remote coordinates so GitHub/ADO/GitLab download paths stay the source of truth).

Growth signal. Same-repo sibling resolution is a concrete enterprise-monorepo adoption unlock, and the scope fence (NOT workspaces, NOT namespaces, NOT cross-repo) is communicated clearly in the PR body, CHANGELOG, and docs -- a clean, low-risk story to amplify in release notes without over-promising.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 1 Clean extraction; repo-root derivation is coupled to install-path depth.
CLI Logging Expert 0 0 1 Rejection message is actionable; one run-on punctuation nit.
DevX UX Expert 0 1 0 Fallback rejection wording is more jargon-y than the expansion-path messages.
Supply Chain Security Expert 0 1 0 Containment fence is sound and fail-closed; add a real-FS symlink-escape proof.
OSS Growth Hacker 0 0 0 Narrow, well-fenced monorepo unlock worth amplifying.
Test Coverage Expert 0 1 0 Same-repo/escape/cross-repo covered; absolute, registry, and fallback branches are not.
Auth Expert 0 1 0 Same-origin reuse is correct; success test mocks the downloader, so reuse is unproven.
Doc Writer 0 0 1 Docs + skill resource + CHANGELOG all updated; consider a security-model line.
Performance Expert 0 0 0 Clone reuse avoids a second fetch; path math is negligible.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Test Coverage Expert] Add rejection tests for the absolute-path, registry-parent, and fallback-loader branches. -- Verified on the head ref that only 3 tests exist (same-repo success, escape, cross-repo); these new security branches have no asserting test, so a silent drift would not fail loudly.
  2. [Supply Chain Security Expert] Add a real-filesystem test that a symlink inside the clone pointing outside the repo root is rejected. -- The anti-exfiltration guard is load-bearing and shared with [FEATURE] fetch path from gitlab repository using git clone instead of API #1014/feat(deps): fetch path-scoped files over git transport instead of host API #1740; current unit tests mock the downloader and exercise only pure-path cases.
  3. [Auth Expert] Add an integration assertion that the sibling fetch reuses the parent's per-dep auth context (same host/token). -- The success test runs against a mocked downloader; an integration proof guards against a future change silently routing the sibling through a different origin or credential.
  4. [Python Architect] Document or guard the coupling between virtual_path segment depth and repo-root derivation in _remote_repo_root_for_parent. -- Containment correctness depends on virtual_path mirroring the on-disk depth; an install-layout change must update this in lockstep or the fence could miscompute.
  5. [DevX UX Expert] Align the fallback loader rejection wording with the clearer expansion-path messages. -- Users who hit the fallback see "could not be tied to its authenticated repo root" instead of the actionable phrasing used elsewhere.

Architecture

classDiagram
    class APMDependencyResolver {
        -Path _apm_modules_dir
        -set _rejected_remote_local_keys
        +build_dependency_tree(root_apm_yml) DependencyTree
        +expand_parent_repo_decl(parent_dep, child_dep) DependencyReference
        -_inherit_remote_parent_fields(parent_dep, child_dep) DependencyReference
        -_is_absolute_local_path(local_path) bool
        -_remote_repo_root_for_parent(parent_dep, parent_pkg) Path
        -_expand_remote_parent_local_path(parent_dep, parent_pkg, child_dep) DependencyReference
        -_reject_remote_parent_local_path(dep_ref, parent_pkg, detail) void
        -_expand_or_reject_remote_parent_local_path(parent_dep, parent_pkg, child_dep) DependencyReference
        -_try_load_dependency_package(dep_ref, parent_pkg) APMPackage
    }
    class DependencyReference {
        +str repo_url
        +str virtual_path
        +str reference
        +bool is_local
        +str local_path
        +bool is_virtual
    }
    class PathSecurity {
        +ensure_path_within(path, base) Path
        +validate_path_segments(path_str, context) void
    }
    APMDependencyResolver ..> DependencyReference : produces
    APMDependencyResolver ..> PathSecurity : enforces containment
Loading
flowchart TD
    A[BFS expands sub_dep] --> B{is_local and remote parent?}
    B -- no --> Z[Use child_dep unchanged]
    B -- yes --> C{registry parent or absolute path?}
    C -- yes --> R[Reject: record key and emit _rich_error]
    C -- no --> D[Compute repo_root from source_path and virtual_path]
    D --> E[ensure_path_within parent_source/local_path inside repo_root]
    E -- escapes root --> R
    E -- inside root --> F[Expand to same-origin virtual dep]
    F --> G[Inherit host/repo/ref, registry_name None]
    G --> H[Downloader reuses existing clone]
    R --> I[Fallback loader reject stays fail-closed]
Loading

Recommendation

Ship now. There are no blocking-severity findings: the same-repo containment boundary is implemented through the sanctioned guards, fails closed, and reuses the parent's authenticated origin. The highest-signal follow-up to track is the test-coverage gap -- the new absolute-path, registry-parent, and fallback-loader rejection branches (plus a real-FS symlink-escape case and an auth-reuse integration assertion) should get targeted tests so this security surface, which #1740 builds on, cannot drift silently.


Full per-persona findings

Python Architect

  • [recommended] Repo-root derivation is coupled to the install-path layout at src/apm_cli/deps/apm_resolver.py
    _remote_repo_root_for_parent walks up one parent per virtual_path segment to find the clone root, then containment is checked against it. This is correct for the current layout, but the security boundary's correctness silently depends on virtual_path faithfully mirroring the on-disk depth of source_path. If the install path layout ever changes, this derivation must change in lockstep or containment could miscompute the root.
    Suggested: Add a short invariant comment (or an assertion that source_path ends with virtual_path) so the coupling is explicit and guarded.
  • [nit] The _inherit_remote_parent_fields extraction is a clean de-duplication of the field-inheritance block and keeps the expansion vs fallback-reject split readable; a one-line module comment naming those two tiers would help future readers.

CLI Logging Expert

  • [nit] Run-on rejection message at src/apm_cli/deps/apm_resolver.py
    The composed error is "{detail} Use a relative path...", but some detail strings have no terminal punctuation (e.g. "absolute paths inside remote packages are not allowed"), producing "...not allowed Use a relative path...". Normalize detail strings to end with a period. The message otherwise names the dependency, the package, the reason, and the fix -- good.

DevX UX Expert

  • [recommended] Inconsistent rejection wording across the two reject paths at src/apm_cli/deps/apm_resolver.py
    The expansion-path PathTraversalError messages are specific and user-readable, but the fallback loader passes "remote package path could not be tied to its authenticated repo root." which is internal jargon. A user who lands on the fallback gets a less actionable message than one who hits the expansion guard.
    Suggested: Reuse the same "stays inside the same remote repo / publish as a standalone package" phrasing for both paths.

Supply Chain Security Expert

  • [recommended] Containment fence is sound; add a real-filesystem escape proof. src/apm_cli/deps/apm_resolver.py
    The boundary correctly routes through ensure_path_within (symlink-resolving, fail-closed) and validate_path_segments, rejects registry and absolute parents, and reuses the parent origin with registry_name=None -- no new credential or origin is introduced, which is the right anti-exfiltration posture. The gap is proof: the existing tests mock the download callback and exercise only string-path cases, so a symlink inside the clone that points outside the repo root is not exercised end-to-end. Given this surface is load-bearing for [FEATURE] fetch path from gitlab repository using git clone instead of API #1014/feat(deps): fetch path-scoped files over git transport instead of host API #1740, a real-FS symlink-escape regression test would lock the guarantee.

OSS Growth Hacker

No findings.

Test Coverage Expert

  • [recommended] New rejection branches lack asserting tests. tests/test_apm_resolver.py
    Verified against the head ref (danielmeppiel/1571-local-deps-shared-repo): the diff adds exactly three test_remote_parent_* tests -- same-repo sibling success, repo-root escape, and cross-repo clone -- which cover the primary boundary well. But the PR also adds an absolute-path rejection branch (_is_absolute_local_path, POSIX + Windows), a registry-parent rejection branch, and the fallback _try_load_dependency_package reject path; none of these has a test that would fail if it silently regressed.
    Suggested: Add targeted rejection tests for an absolute path: (e.g. /etc/x and a Windows C:\\x), a registry-parent declaring a path:, and a dep that reaches the fallback loader.

Auth Expert

  • [recommended] Same-origin reuse is correct but unproven by the success test. src/apm_cli/deps/apm_resolver.py
    Expansion inherits the parent's repo_url/host/ref and resets registry_name=None, so the sibling is fetched under the same authenticated origin and shared clone with no second credential resolution -- the safe design that avoids origin confusion. However, the success test asserts repo_url/ref/virtual_path against a mocked downloader, so it does not prove the real per-dep auth context is reused for the sibling fetch.
    Suggested: Add an integration-level assertion that the sibling resolves through the same host/token as the parent, so a future change cannot silently route it through a different credential.

Doc Writer

  • [nit] Docs are consistent and Rule 4 is satisfied: docs/src/content/docs/consumer/manage-dependencies.md, docs/src/content/docs/reference/manifest-schema.md, the packages/apm-guide/.apm/skills/apm-usage/dependencies.md skill resource, and CHANGELOG.md all describe the same-repo-only boundary. Consider adding one line to the security model doc (docs/src/content/docs/enterprise/security.md) so the path-safety contract mirrors the new expansion/rejection behavior.

Performance Expert

No findings.

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel merged commit 1f6d522 into main Jun 11, 2026
15 checks passed
@danielmeppiel danielmeppiel deleted the danielmeppiel/1571-local-deps-shared-repo branch June 11, 2026 07:05
danielmeppiel added a commit that referenced this pull request Jun 11, 2026
…t API

For GitLab-sourced deps (SaaS or self-hosted), path: file fetches now go
through git sparse/partial checkout (--filter=blob:none + cone sparse-checkout)
rather than the host REST API. This is the primary path and fixes self-hosted
GitLab instances that return HTTP 410 (REST API disabled), because git
transport reuses the same SSH keys and git credential helpers already in use
for clones.

Design (ratified by board decision):
- git-transport-first: fetch_file_via_git_sparse() in the new
  deps/git_file_transport.py module performs git init + remote add +
  sparse-checkout init --cone + fetch --filter=blob:none --depth=1 + checkout
- GITLAB_APM_PAT fallback: if git transport raises, DownloadDelegate.download_gitlab_file()
  falls back to the existing GitLab REST v4 API (mirrors ADO_APM_PAT pattern)
- Path containment: ensure_path_within() is applied to the materialized file
  path after checkout, rejecting symlink/traversal escapes from cloned repos
- validate_path_segments() rejects .. traversal sequences before any git work
- Reuses the existing clone auth environment (git_env from the downloader);
  no second origin or cached clone

TDD coverage (tests/test_gitlab_git_transport.py):
- Acceptance: git-sourced dep with path: fetches file via git (no REST API call)
- Self-hosted GitLab API-410 no longer fails
- ensure_path_within() rejects symlink-escaping path: after checkout
- validate_path_segments() rejects .. traversal before git work
- GITLAB_PAT fallback path exercised when git transport raises
- Root-level files skip cone setup (no sparse-checkout init)
- git failure raises RuntimeError with descriptive message
- File missing after checkout raises RuntimeError

Docs: updated consumer/authentication.md, consumer/manage-dependencies.md,
apm-usage/authentication.md, apm-usage/dependencies.md, and CHANGELOG.md.

Stacked on #1732 (#1571). Closes #1014.

Credit: @cossons for root-cause analysis of the self-hosted GitLab 410 repro
and the ratified design (git-transport-first + thin PAT fallback).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel added a commit that referenced this pull request Jun 11, 2026
…t API

For GitLab-sourced deps (SaaS or self-hosted), path: file fetches now go
through git sparse/partial checkout (--filter=blob:none + cone sparse-checkout)
rather than the host REST API. This is the primary path and fixes self-hosted
GitLab instances that return HTTP 410 (REST API disabled), because git
transport reuses the same SSH keys and git credential helpers already in use
for clones.

Design (ratified by board decision):
- git-transport-first: fetch_file_via_git_sparse() in the new
  deps/git_file_transport.py module performs git init + remote add +
  sparse-checkout init --cone + fetch --filter=blob:none --depth=1 + checkout
- GITLAB_APM_PAT fallback: if git transport raises, DownloadDelegate.download_gitlab_file()
  falls back to the existing GitLab REST v4 API (mirrors ADO_APM_PAT pattern)
- Path containment: ensure_path_within() is applied to the materialized file
  path after checkout, rejecting symlink/traversal escapes from cloned repos
- validate_path_segments() rejects .. traversal sequences before any git work
- Reuses the existing clone auth environment (git_env from the downloader);
  no second origin or cached clone

TDD coverage (tests/test_gitlab_git_transport.py):
- Acceptance: git-sourced dep with path: fetches file via git (no REST API call)
- Self-hosted GitLab API-410 no longer fails
- ensure_path_within() rejects symlink-escaping path: after checkout
- validate_path_segments() rejects .. traversal before git work
- GITLAB_PAT fallback path exercised when git transport raises
- Root-level files skip cone setup (no sparse-checkout init)
- git failure raises RuntimeError with descriptive message
- File missing after checkout raises RuntimeError

Docs: updated consumer/authentication.md, consumer/manage-dependencies.md,
apm-usage/authentication.md, apm-usage/dependencies.md, and CHANGELOG.md.

Stacked on #1732 (#1571). Closes #1014.

Credit: @cossons for root-cause analysis of the self-hosted GitLab 410 repro
and the ratified design (git-transport-first + thin PAT fallback).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel added a commit that referenced this pull request Jun 11, 2026
…t API (#1740)

* feat(deps): fetch path-scoped files over git transport instead of host API

For GitLab-sourced deps (SaaS or self-hosted), path: file fetches now go
through git sparse/partial checkout (--filter=blob:none + cone sparse-checkout)
rather than the host REST API. This is the primary path and fixes self-hosted
GitLab instances that return HTTP 410 (REST API disabled), because git
transport reuses the same SSH keys and git credential helpers already in use
for clones.

Design (ratified by board decision):
- git-transport-first: fetch_file_via_git_sparse() in the new
  deps/git_file_transport.py module performs git init + remote add +
  sparse-checkout init --cone + fetch --filter=blob:none --depth=1 + checkout
- GITLAB_APM_PAT fallback: if git transport raises, DownloadDelegate.download_gitlab_file()
  falls back to the existing GitLab REST v4 API (mirrors ADO_APM_PAT pattern)
- Path containment: ensure_path_within() is applied to the materialized file
  path after checkout, rejecting symlink/traversal escapes from cloned repos
- validate_path_segments() rejects .. traversal sequences before any git work
- Reuses the existing clone auth environment (git_env from the downloader);
  no second origin or cached clone

TDD coverage (tests/test_gitlab_git_transport.py):
- Acceptance: git-sourced dep with path: fetches file via git (no REST API call)
- Self-hosted GitLab API-410 no longer fails
- ensure_path_within() rejects symlink-escaping path: after checkout
- validate_path_segments() rejects .. traversal before git work
- GITLAB_PAT fallback path exercised when git transport raises
- Root-level files skip cone setup (no sparse-checkout init)
- git failure raises RuntimeError with descriptive message
- File missing after checkout raises RuntimeError

Docs: updated consumer/authentication.md, consumer/manage-dependencies.md,
apm-usage/authentication.md, apm-usage/dependencies.md, and CHANGELOG.md.

Stacked on #1732 (#1571). Closes #1014.

Credit: @cossons for root-cause analysis of the self-hosted GitLab 410 repro
and the ratified design (git-transport-first + thin PAT fallback).

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

* feat(deps): friendly error for subpath embedded in git URL (#872)

Adds a parse-time guard that detects when a user embeds an APM primitive
subpath inside an explicit git URL form (SCP git@host:path, ssh://, or
https://), e.g.:

  git: git@github.com:org/repo/skills/hello-world.git

Such URLs cause git to fail later with a cryptic 'not a valid repository
name' error. The guard detects any known APM primitive directory name
(skills, agents, prompts, instructions, chatmodes, collections, contexts,
memory) appearing as a non-last interior path segment of an explicit git
URL, and raises a friendly actionable error pointing at the supported
`path:` key instead:

  [x] A subpath cannot be embedded in a git URL. Use the `path:` key
  instead: `git: <repo-url>` + `path: <primitive>/<name>`
  (or the shorthand `org/repo/<primitive>/<name>`).

The monorepo-skill-install capability already ships via the `path:` key
and the bare shorthand (e.g. `org/repo/skills/hello-world`). No new
install feature is added.

False-positive check:
- GitLab subgroups (git@gitlab.com:group/subgroup/repo.git): safe, no
  primitive segment in interior
- Azure DevOps org/project/repo forms: safe, ADO paths never use
  primitive names
- Plain valid git URLs (2-segment path): safe, length guard returns early
- Bare shorthand with subpath (org/repo/skills/foo): safe, not an
  explicit git URL form

The guard is implemented as DependencyReference._check_no_embedded_subpath
and called from DependencyReference.parse() after the shorthand-alias and
fqdn-conflict guards, before virtual-package detection.

Refs #872
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(deps): hard-fail path traversal and attribute GitLab transport

Two panel follow-ups on the #1014 GitLab git-transport surface.

Supply-chain: download_gitlab_file caught a bare `except Exception`
around the git-transport attempt, so a PathTraversalError raised by
the path-validation / symlink-containment guards in
fetch_file_via_git_sparse was silently swallowed and the request fell
through to the REST transport. A rejected traversal must not gain a
second transport to probe. PathTraversalError now propagates unchanged;
only genuine transport failures fall back to REST.

CLI logging: the git->REST fallback transition was `_debug`-only, so
triagers had no signal which transport answered a 410. The fallback now
emits a verbose `[i]` note carrying the git failure reason, and the REST
success notes attribute the GitLab REST API explicitly.

Adds a regression test asserting a PathTraversalError in the git path is
not swallowed into the REST fallback (mutation-break verified). Updates
existing verbose-callback tests that exercised the unmocked-git fallback
path to the new transport-attributed messages.

addresses CEO follow-up (Supply Chain) and (CLI Logging) on PR #1740

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

* fix(deps): scope embedded-subpath guard against false positives

The #872 guard `_check_no_embedded_subpath` flagged any non-last path
segment matching an APM primitive directory name, which produced two
false positives on legitimate URLs:

- A GitLab subgroup literally named after a primitive, e.g.
  `git@gitlab.com:group/skills/repo.git`, where `skills` is a subgroup
  at index 1 and `repo` is the real repository.
- Azure DevOps virtual-path URLs, e.g.
  `https://dev.azure.com/org/proj/_git/repo/instructions/x`, where the
  primitive segment is the supported ADO virtual path after `_git/repo`.

The embedded-subpath shape is `org/repo` + `<primitive>/<name>`, so a
primitive segment now only fires when preceded by a complete org/repo
prefix (index >= 2), and ADO URLs (identified by the `_git` marker) are
skipped entirely. The canonical malformed form `org/repo/skills/<name>`
that #872 targets still hard-fails; no GitHub or GitLab repo path uses
`_git`, so real detection is unaffected. A residual deep-subgroup
ambiguity (`group/sub/skills/repo`) is documented in code.

Adds a regression test for the primitive-named subgroup (mutation-break
verified); the ADO false positive is covered by the existing
TestParseAdoUrls suite.

addresses CEO follow-up (DevX) on PR #1740; Refs #872

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

* fix(deps): harden gitlab fallback diagnostics

Avoid echoing raw git transport exceptions during the GitLab REST fallback and keep fallback tests hermetic by mocking the git transport failure path. Skip the symlink containment test when the platform cannot create symlinks.

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

* Fold GitLab git transport panel follow-ups

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

* Fold second GitLab transport panel pass

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

* Fold final GitLab transport panel nits

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

* Fold residual GitLab transport polish

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

* refactor(deps): extract identity/semver helpers to identity.py

main tightened the file-length guardrail from 2450 to 2100 lines (Stage 1);
after merging main, reference.py was 2191 lines. Move the pure, stateless
identity helpers (build_dependency_unique_key, build_canonical_dependency_string,
_path_segment_pattern, the semver-range guards + segment-pattern constants) into
a sibling models/dependency/identity.py and re-export them from reference.py so
deps/lockfile.py and install/phases/resolve.py import sites stay unchanged.
Behavior-preserving; reference.py now 2099 lines (under guardrail).

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

---------

Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sergio-sisternes-epam pushed a commit that referenced this pull request Jun 13, 2026
Sync the Stage 2 complexity/file-length refactor branch with main's 22
feature commits (Hermes #1726, Kiro IDE #1741, multi-host dep identity
#1735, same-repo remote path deps #1732, git_file_transport #1740,
revision pins #1738, marketplace sourceBase/source parity/inherit
description #1736/#1739/#1755, pack --archive .zip #1720, mcp optional
registry inputs #1734, and the v0.19.0/v0.20.0 releases).

Conflict resolution preserved both sides: main's new features ported
through the branch's extracted sibling modules, branch's tightened ruff
thresholds (max-statements=120, max-branches=40, max-complexity=35,
max-returns=8, max-args=12) and 800-line file limit retained.

All 7 CI-mirror lint gates pass; full unit suite green (17099 passed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] or [QUESTION] Local dependencies in shared repository

2 participants