fix(alias): reject path-traversing dependency aliases — fixes #2900 - #2901
fix(alias): reject path-traversing dependency aliases — fixes #2900#2901Daniel (Danvs60) wants to merge 10 commits into
Conversation
Aliases were only matched against a lax character regex, letting values like "..", "./x", or "foo/../bar" escape apm_modules at download and integrate time. Now validating every parsed alias through path_security's segment checks and guarding the resolved install path inside the download/integrate phases. Adds unit coverage for traversal and safe aliases.
Wrap validate_path_segments in parse_alias_override so '.'/'..' aliases translate PathTraversalError into the existing allowed-character message instead of leaking a low-level technical error. Strengthen traversal tests to assert the friendly message.
A valid alias like 'safe-name' passes parse-time validation, but apm_modules_dir/safe-name can itself be a symlink pointing outside apm_modules_dir. ensure_path_within is the only guard that resolves symlinks before containment (download.py:65, integrate.py:622); this test proves the escape raises PathTraversalError and never writes outside the managed tree.
The parser rejects traversal aliases (parse_alias_override), and the
ensure_path_within containment guards exist at download.py:65 and
integrate.py:622 as the defense-in-depth last line. But no test drove
those
guards through the real phase entry points -- the PR "Scenario 4" claim
(install path can never escape apm_modules even if the parser is
bypassed)
was proven only at unit tier.
Add install-tier regression traps that route malicious aliases through
the
actual phase run() functions:
- tests/red_team/install/test_alias_path_escape.py
* download.run() rejects a '..' traversal alias and a symlink that
resolves outside apm_modules_dir via PathTraversalError, asserting
no
download bytes land.
* integrate.run() rejects the same two vectors before materialization.
* Safe-alias controls confirm no false positive.
- tests/unit/test_registry_entry_alias_traversal.py
* Covers the secondary parse_registry_object_entry alias validation
(registry_entry.py:86): regex layer (%2e%2e) and
validate_path_segments
layer ('..', 'pkg/..'), plus a safe-alias affirmative control.
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
🟡 Changes recommended
One new registry-alias test does not actually exercise the intended validation layer, and a couple of new containment-guard call sites rely on type: ignore where a small explicit non-None guard would make the contract and types correct.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR closes a path traversal / alias poisoning hole by rejecting traversal-like dependency aliases at parse time and adding defense-in-depth containment checks when constructing alias-derived install paths during apm install.
Changes:
- Add
validate_path_segments(..., context="dependency alias")to alias parsing for object-form dependencies (including registry object entries). - Add
ensure_path_within()containment checks at the two alias join sites in the download and integrate phases. - Add unit and red-team regression tests covering traversal aliases and symlink-based escapes.
File summaries
| File | Description |
|---|---|
| tests/unit/test_registry_entry_alias_traversal.py | New unit tests targeting registry object-entry alias validation. |
| tests/unit/test_alias_traversal.py | New unit tests for alias override parsing and ensure_path_within symlink containment. |
| tests/red_team/install/test_alias_path_escape.py | New red-team tests driving install phases directly to ensure containment guards trip before writes. |
| src/apm_cli/models/dependency/registry_entry.py | Add validate_path_segments check for registry entry aliases. |
| src/apm_cli/models/dependency/object_fields.py | Add validate_path_segments check for alias overrides and map traversal rejection into ValueError. |
| src/apm_cli/install/phases/integrate.py | Add ensure_path_within guard when alias is used to build install_path. |
| src/apm_cli/install/phases/download.py | Add ensure_path_within guard when alias is used to build pre-download path. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| CLI Logging Expert | 0 | 0 | 0 | Safe alias diagnostics remain actionable; cache admission now distinguishes reuse from replacement without adding output noise. No CLI-logging findings. |
| DevX UX Expert | 0 | 0 | 0 | Both composition repairs address the reproduced DevX faults; safe aliases, unchanged local sources and non-destructive errors remain covered. Exact-head verification is still pending. |
| Doc Writer | 0 | 0 | 0 | Unchanged docs match ae3099d: the bounded repairs preserve existing semantics without a new interface or inventory-as-identity claim. Final PR-body evidence remains driver-owned. |
| OSS Growth Hacker | 0 | 0 | 0 | No new growth findings. Credited release guidance and non-destructive migration remain appropriate; the bounded composition repairs need no additional onboarding or launch surface. |
| performance-expert | 0 | 0 | 0 | No actionable performance regression: reuse-only validation preserves fetch decisions; remote alias anchoring adds bounded path checks, not downloads or tree copies. |
| Python Architect | 0 | 0 | 1 | Both round3 composition faults now route through existing owners. The final focused gate passed; no remaining correctness or compounding architecture fault was found in scope. |
| Supply Chain Security | 0 | 0 | 0 | Both round3 faults are repaired at the intended boundaries; retained containment, provenance and cache-admission controls show no new scoped security concern. |
| Test Coverage | 0 | 1 | 0 | Both composition gaps have regression traps: 13 independent probes pass; exact-head gate records 588 passes plus 9 subtests. Only the owned Scenario Evidence update remains. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Architecture
### 1. OO / module boundaries - Before round4
```mermaid
classDiagram
direction LR
class DependencyReference {
<<Dataclass>>
+get_install_path(modules) Path
}
class APMPackage {
<<Dataclass>>
+source_path Path
+package_path Path
}
class APMDependencyResolver {
+_try_load_dependency_package()
+_remote_repo_root_for_parent() Path
+_expand_remote_parent_local_path()
}
class DownloadCallback {
<<Protocol>>
+__call__()
}
class ActivationCallback {
<<Protocol>>
+__call__()
}
class LegacyPluginCompat {
<<IOBoundary>>
+validate_cached_legacy_plugin()
+upgrade_cached_legacy_plugin()
}
ProviderCoordinateMixin <|-- DependencyReference
DependencyReference ..> ObjectFields : object and SSH aliases
RegistryEntry ..> ObjectFields : alias parser
DependencyReference ..> MaterializationPaths : destination owner
MaterializationPaths ..> ObjectFields : defensive validation
MaterializationPaths ..> PathSecurity : strict resolved descent
ResolvePhase ..> MaterializationPaths : prepare destinations
ResolvePhase ..> LegacyPluginCompat : preflight BEFORE resolver
ResolvePhase ..> APMDependencyResolver : constructs
APMDependencyResolver o-- DownloadCallback : injected fetch
APMDependencyResolver o-- ActivationCallback : injected publish
APMDependencyResolver ..> APMPackage : physical source_path
APMDependencyResolver ..> DependencyReference : identity and destination
APMDependencyResolver ..> PathSecurity : remote containment
DownloadPhase ..> DependencyReference : get_install_path
IntegratePhase ..> DependencyReference : get_install_path
DependencySource <|-- CachedDependencySource
DependencySource <|-- FreshDependencySource
DependencySource <|-- LocalDependencySource
IntegratePhase ..> DependencySource : make_dependency_source
CachedDependencySource ..> LegacyPluginCompat : transactional upgrade
LegacyPluginCompat ..> ResolutionStagingSession : normalize safely
note for APMDependencyResolver "Remote repo root subtracts virtual depth from physical alias destination"
note for ResolvePhase "Legacy preflight runs before knowing whether cached bytes will be discarded"
1. OO / module boundaries - After round4
classDiagram
direction LR
class DependencyReference {
<<Dataclass>>
+get_install_path(modules) Path
}
class APMPackage {
<<Dataclass>>
+source_path Path
+package_path Path
}
class APMDependencyResolver {
<<CallbackInjection>>
+_try_load_dependency_package()
+_remote_source_paths_for_parent() tuple
+_expand_remote_parent_local_path()
}
class DownloadCallback {
<<Protocol>>
+__call__()
}
class ActivationCallback {
<<Protocol>>
+__call__()
}
class LegacyPluginCompat {
<<IOBoundary>>
+validate_cached_legacy_plugin()
+upgrade_cached_legacy_plugin()
}
ProviderCoordinateMixin <|-- DependencyReference
DependencyReference ..> ObjectFields : object and SSH aliases
RegistryEntry ..> ObjectFields : alias parser
DependencyReference ..> MaterializationPaths : destination owner
MaterializationPaths ..> ObjectFields : defensive validation
MaterializationPaths ..> PathSecurity : normalize destination AND root
ResolvePhase ..> MaterializationPaths : prepare destinations only
ResolvePhase ..> APMDependencyResolver : constructs and injects partial
ResolvePhase ..> LegacyPluginCompat : binds read-only callback
APMDependencyResolver ..> LegacyPluginCompat : invokes callback, no import
APMDependencyResolver o-- DownloadCallback : injected fetch
APMDependencyResolver o-- ActivationCallback : injected publish
APMDependencyResolver ..> APMPackage : validate actual source_path
APMDependencyResolver ..> DependencyReference : replace alias=None for coordinates
APMDependencyResolver ..> PathSecurity : remote containment
DownloadPhase ..> DependencyReference : get_install_path
IntegratePhase ..> DependencyReference : get_install_path
DependencySource <|-- CachedDependencySource
DependencySource <|-- FreshDependencySource
DependencySource <|-- LocalDependencySource
IntegratePhase ..> DependencySource : make_dependency_source
CachedDependencySource ..> LegacyPluginCompat : transactional upgrade
LegacyPluginCompat ..> ResolutionStagingSession : normalize safely
note for ObjectFields "Guard Clauses: one alias vocabulary; reject exactly bare dots"
note for MaterializationPaths "Guard Clauses and Single-owner delegation: strict destination; explicit alias skips case migration"
note for APMDependencyResolver "Dependency Injection: read-only cache callback AFTER fetch decision, BEFORE normalization; remote coordinates reuse unaliased destination owner"
note for LegacyPluginCompat "Extract Function: one admission policy for reused resolver bytes and cached upgrade"
class DependencyReference:::touched
class ObjectFields:::touched
class RegistryEntry:::touched
class MaterializationPaths:::touched
class ResolvePhase:::touched
class APMDependencyResolver:::touched
class DownloadPhase:::touched
class IntegratePhase:::touched
class LegacyPluginCompat:::touched
classDef touched fill:#fff3b0,stroke:#d47600
```mermaid
### 2. Execution flow - Before round4
```mermaid
flowchart TD
A["[LOCK] commands/install.py::install\nserialized_lifecycle_unless"] --> R["[I/O] [FS] install/phases/resolve.py::run\n_load_lockfile; _ensure_modules_dir\n_prepare_existing_materialization_paths"]
R --> O["[I/O] models/dependency/materialization.py::prepare_materialization_path\nget_install_path -> build_materialization_path\nstrict alias destination; skip alias case migration"]
O -->|invalid destination| E["[FS] commands/install.py: transaction.fail\napply_install_command_outcome; ctx.exit: 1"]
O --> P{"[I/O] resolve.py::_prepare_existing_materialization_paths\ndestination.exists()?"}
P -->|yes| V["[I/O] install/legacy_plugin_compat.py::validate_cached_legacy_plugin\npreflight before fetch decision"]
V -->|invalid old metadata, even if replacement needed| E
P -->|no| L
V -->|valid or ineligible| L["[I/O] deps/apm_resolver.py::_try_load_dependency_package\nget_install_path"]
L --> F{"[I/O] local OR missing path OR\n_should_force_recheck(dep_ref)?"}
F -->|yes| D["[LOCK] [NET] [FS] _try_load_dependency_package reserves key\nresolve.py::download_callback\nprepare_replacement; download_package"]
D -->|usable candidate| M
D -->|failed required dependency| E
F -->|no| M["[I/O] [FS] deps/apm_resolver.py\nmaterialize_marketplace_manifest\nvalidate_apm_package / APMPackage.from_apm_yml"]
M -->|invalid| E
M -->|valid| Q["[FS] apm_resolver.py::_activate_validated_package\nresolve.py::_activate_validated_candidate for staged bytes"]
Q --> C{"apm_resolver.py BFS\nremote parent declares relative path?"}
C -->|yes| S["[I/O] _remote_repo_root_for_parent\nphysical parent.source_path minus virtual depth"]
S -->|flat alias makes root escape modules| E
S -->|contained| H["[I/O] _expand_remote_parent_local_path\nensure_path_within(child, repo_root)\n_inherit_remote_parent_fields"]
H -->|valid remote child| L
H -->|outside repo| E
C -->|local parent| LC["[I/O] _compute_dep_source_path\noriginal local source remains anchor"]
LC --> L
C -->|graph complete| I["[I/O] [NET] [FS] download.py::run; integrate.py::run\neach uses get_install_path\nCachedDependencySource.acquire -> upgrade_cached_legacy_plugin"]
I --> K["[FS] install/template.py::run_integration_template\n[LOCK] phases/lockfile.py::LockfileBuilder.build_and_save"]
K --> Z["commands/install.py\napply_install_command_outcome; ctx.exit: 0 on success"]
2. Execution flow - After round4
flowchart TD
A["[LOCK] commands/install.py::install\nserialized_lifecycle_unless"] --> R["[I/O] [FS] install/phases/resolve.py::run\n_load_lockfile; _ensure_modules_dir\n_prepare_existing_materialization_paths"]
R --> O["[I/O] models/dependency/materialization.py::prepare_materialization_path\nget_install_path -> build_materialization_path\nparse_alias_override; ensure_path_within both operands\nstrict alias destination; skip alias case migration"]
O -->|invalid destination| E["[FS] commands/install.py: transaction.fail\napply_install_command_outcome; ctx.exit: 1"]
O --> W["resolve.py::_resolve_dependencies\ninject partial(validate_cached_legacy_plugin,\nlockfile=existing_lockfile, fetched_this_run=False)"]
W --> L["[I/O] deps/apm_resolver.py::_try_load_dependency_package\nget_install_path"]
L --> F{"[I/O] local OR missing path OR\n_should_force_recheck(dep_ref)?"}
F -->|yes| D["[LOCK] [NET] [FS] _try_load_dependency_package reserves key\nresolve.py::download_callback\nprepare_replacement; download_package"]
D -->|usable path| U
D -->|failed required dependency| E
F -->|no| U{"apm_resolver.py:1204\ncallback present AND dedup key NOT in\n_downloaded_packages?"}
U -->|yes: reused key| V["[I/O] install/legacy_plugin_compat.py::validate_cached_legacy_plugin\nreceiptless locked 0.28 marketplace eligibility\nmetadata, symlink and hash checks"]
U -->|no: downloaded key or no callback| M
V -->|invalid reused metadata| E
V -->|valid or ineligible| M["[I/O] [FS] deps/apm_resolver.py\nmaterialize_marketplace_manifest\nvalidate_apm_package / APMPackage.from_apm_yml"]
M -->|invalid| E
M -->|valid| Q["[FS] apm_resolver.py::_activate_validated_package\nresolve.py::_activate_validated_candidate for staged bytes"]
Q --> C{"apm_resolver.py BFS\nremote parent declares relative path?"}
C -->|yes| S["[I/O] apm_resolver.py::_remote_source_paths_for_parent\ncontain actual parent.source_path within modules"]
S --> PA{"parent_dep.alias?"}
PA -->|yes| SA["[I/O] replace(parent_dep, alias=None).get_install_path\ncontain source-coordinate anchor within modules\nNO source copy or local-read fallback"]
PA -->|no| SN["Use contained actual parent source anchor"]
SA --> SR
SN --> SR["[I/O] validate_path_segments(virtual_path)\nderive repository root by virtual depth\nensure_path_within(repo_root, modules)"]
SR --> H["[I/O] _expand_remote_parent_local_path\nensure_path_within(parent_source, repo_root)\nensure_path_within(child, repo_root)"]
H -->|outside repo| E
H -->|contained sibling or repo root| IN["_inherit_remote_parent_fields\nretain host, port, repo, ref\nis_local=False; local_path=None"]
IN --> L
C -->|local parent| LC["[I/O] _compute_dep_source_path\noriginal local source remains anchor"]
LC --> L
C -->|graph complete| I["[I/O] [NET] [FS] download.py::run; integrate.py::run\neach uses get_install_path\nCachedDependencySource.acquire -> upgrade_cached_legacy_plugin\nshared validator; transactional normalization when eligible"]
I --> K["[FS] install/template.py::run_integration_template\n[LOCK] phases/lockfile.py::LockfileBuilder.build_and_save"]
K --> Z["commands/install.py\napply_install_command_outcome; ctx.exit: 0 on success"]
### Recommendation
Code-readiness advisory only for ae3099de229141401b770af29c32dc0f63510f9b: no further in-scope code folds remain. This stance is not approval, merge authorization or a claim of green GitHub CI. The driver terminal awaits maintainer CI action because six exact-head Actions runs require maintainer action and have no jobs; only CLA has succeeded. The maintainer must address that external CI state, starting with https://github.com/microsoft/apm/actions/runs/34231025610, then evaluate the actual required checks and make the human ship decision. Do not bypass check policy or use the fourth-round cap as a reason to ship.
---
<details>
<summary>Full per-persona findings</summary>
#### Python Architect
- **[nit]** Retain the existing owners and callback seam; no further abstraction is needed at `src/apm_cli/deps/apm_resolver.py:1204`
Architecture assessment only, not a requested code change. The source-coordinate repair extends the resolver's existing remote-parent authority and obtains the unaliased package coordinate through DependencyReference.get_install_path. It does not invent another repository-layout algorithm. The compatibility check remains in legacy_plugin_compat.py and is injected into the resolver, so deps does not import install or duplicate legacy eligibility. The resolver retains the existing ref-drift and download decision before the new reuse check. This corrects my round3 assessment: unchanged individual owners were not sufficient to establish that alias destinations composed correctly with remote source anchoring or replacement fetches.
**Design patterns**
- Used in this PR: Guard Clauses -- ObjectFields and MaterializationPaths reject reserved aliases and non-descendant destinations before returning an install path.
- Used in this PR: Single-owner delegation -- alias ingress, install phases and remote coordinate recovery route through their existing lexical, destination and containment owners.
- Used in this PR: Dependency Injection -- ResolvePhase supplies the read-only compatibility callback to APMDependencyResolver using the established callback seam.
- Used in this PR: Extract Function -- LegacyPluginCompat shares one read-only admission function between resolver cache reuse and transactional cached upgrade.
- Pragmatic suggestion: none -- the current shape is the simplest correct design at this scope.
#### CLI Logging Expert
No findings.
#### DevX UX Expert
No findings.
#### Supply Chain Security
No findings.
#### OSS Growth Hacker
No findings.
#### Auth Expert -- inactive
Changes in src/apm_cli/deps/apm_resolver.py, src/apm_cli/models/dependency/{reference,object_fields,registry_entry,materialization}.py, src/apm_cli/install/phases/{resolve,download,integrate}.py and src/apm_cli/install/legacy_plugin_compat.py change alias destinations, source-coordinate anchoring and cache admission without changing authentication inputs, host classification, credential resolution, authorization or remote-host fallback semantics.
#### Doc Writer
No findings.
#### Test Coverage
- **[recommended]** Complete the already-owned Scenario Evidence update with final-head results.
The supplied historical body in pr-2901-context.json has testing checkboxes but no Scenario Evidence table. This carries forward the acknowledged driver-owned writing action, not a new production defect. Read the scenario-evidence rubric and audited the full current diff. The new aliased remote-parent and invalid-cache/ref-change scenarios now have real regression traps; narrower round3 passing tests did not cover those combinations. The completed terminal-head summary is 588 passed, 9 subtests passed in 279.68s. The body must distinguish this focused result from the prior static-mutation failure, the unrelated broader-run path-sensitive failure, and unverified GitHub CI.
*Suggested:* Map the two new composition regressions and retained alias, source-boundary, inventory/hash and no-mutation promises to user-worded scenarios, principles and real test identifiers. Mark the #2900 regression traps. Attribute red/green and mutation evidence separately from final-head execution. Preserve the owned historical corrections: only bare dots are newly rejected, slash/percent inputs were already invalid, sibling support did not introduce the bypass, and inventory fields are not canonical identity.
*Proof (test passed):* `tests/integration/test_required_lifecycle_state_machine.py::test_required_invalid_receiptless_legacy_cache_fails_with_recovery[replacement-missing-apm-yml]` -- proves: Changing my requested ref can replace an invalid old cache and restore the installed package and deployed skills. [portability-by-manifest,secure-by-default,devx]
`assert receipt.is_file() assert replaced.content_hash == compute_package_hash(cached_package) assert deployed.read_text(encoding="utf-8") == _skill("legacy-skill")`
</details>
<sub>This panel is advisory. It does not block merge. Re-apply the `panel-review` label after addressing feedback to re-run.</sub>
Address the PR microsoft#2901 panel and Copilot follow-ups: route all alias ingress and materialization through existing owners, reject root-equal destinations, preserve local sibling sources, and defend real reinstall metadata and hashes with regression and architecture tests. Include actionable diagnostics and migration guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve e845ebf and its explicit non-None contract through the phase-level RuntimeError guard. The shared materialization owner replaces the parallel alias join. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address round-two architecture/security and lifecycle findings. Compare both roots through path_security, give safe alias recovery guidance, and preserve existing legacy-plugin validation before alias-aware resolution can normalize cached files. Main passes the legacy missing-metadata cases; added preflight keeps that behavior. Windows prefix and legacy-preflight mutation controls fail with guards removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep authenticated remote coordinates separate from flat aliases. Inject the existing read-only legacy cache admission at actual reuse after canonical fetch decisions, not preparation; retain same-ref failure and transactional replacement. Extend real resolver and CLI lifecycle contracts with aliased remote siblings and invalid-cache ref changes. Mutation controls detect both source-anchor regression and misplaced or missing admission. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Shepherd driver: code complete; CI requires maintainer actionPushed External stop: all six exact-head Actions runs report The fourth real expert panel and CEO recommend What was folded
Binding reservations and disposition
Verification and limits
Terminal classification: blocked solely on maintainer CI action, not on unimplemented code. Four outer iterations, two Copilot fetch/classification rounds, zero CI repair commits. No fifth review loop, auto-approval or auto-merge. |
fix(alias): contain install destinations without restricting source paths
TL;DR
Reject aliases that select
apm_modulesitself or escape it, and route alias parsing and destination selection through their existing owners. Preserve.safe,safe.,foo..bar,my-skill.v2, local transitive../chains, and same-repository remote siblings. Extend Daniel (@Danvs60)'s fix with real install/reinstall, inventory/hash, source-composition and legacy-cache regression coverage.Fixes #2900.
Problem (WHY)
.selected the modules root;..selected its parent. Generic containment alone permits equality, so a root-pointing alias symlink also needs explicit exclusion.nameand manifestversionare inventory fields, not canonical dependency identity, deduplication or trust keys.../references resolve from their declaring package; remote siblings retain their authenticated repository coordinates. Cache validation must likewise distinguish reused bytes from bytes a requested ref change will replace.These are concrete production-path failures and compatibility constraints, not a ban on dotted names. The regression process follows Agent Skills' "do the work, run a validator (a script, a reference checklist, or a self-check), fix any issues, and repeat until validation passes.". Fragile ordering is deliberately explicit: "when operations are fragile, consistency matters, or a specific sequence must be followed".
Why this could appear safe before: historical trace
c50f223a(2025-09-18), already accepted dot aliases and joined aliases directly.get_install_path, but the independent alias branches bypassed it. This was an old bypass, not a new effect of sibling support.apm pack --format plugin,apm init --plugin, devDependencies #379 (2026-03-20) added cached hash inputs; feat(lockfile): record installed package name + version per entry (#1888) #1904 (2026-06-25) exposed incorrectname/versioninventory when the wrong manifest was read..and.., not otherwise-safe dotted names.Approach (WHAT)
parse_alias_overridefor object, registry and SSH/SCP alias ingress.get_install_path/build_materialization_pathown alias selection, lexical validation and strict resolved containment; both install consumers delegate.Implementation (HOW)
Review basis:
ae3099de2, retaining the contributor's original commits and follow-up.src/apm_cli/models/dependency/object_fields.pysrc/apm_cli/models/dependency/reference.pysrc/apm_cli/models/dependency/registry_entry.pysrc/apm_cli/models/dependency/materialization.pysrc/apm_cli/install/phases/download.py,integrate.pysrc/apm_cli/deps/apm_resolver.pysrc/apm_cli/install/legacy_plugin_compat.py,phases/resolve.py.apm/architecture/owners/contracts-tooling.json,scripts/architecture_linter/checks/contracts_test_taxonomy.pytests/unit/test_alias_traversal.py,test_registry_entry_alias_traversal.py,tests/red_team/install/test_alias_path_escape.pytests/integration/test_architecture_dependency_reference.pytests/integration/test_required_lifecycle_state_machine.pytests/integration/test_transitive_chain_e2e.py,tests/test_apm_resolver.pydocs/src/content/docs/reference/manifest-schema.md,troubleshooting/migration.md,consumer/manage-dependencies.mdpackages/apm-guide/.apm/skills/apm-usage/dependencies.md,CHANGELOG.mdDiagrams
Legend: install consumers share one destination authority; remote source coordinates are independent of alias layout.
flowchart LR subgraph Ingress["Alias ingress"] O["object_fields.parse_alias_override"] R["DependencyReference.parse"] G["registry_entry parser"] R --> O G --> O end subgraph Destination["Destination authority"] D["download.run"] I["integrate dependency dispatch"] P["DependencyReference.get_install_path"] M["build_materialization_path"] C["ensure_path_within and strict root exclusion"] D --> P I --> P P --> M --> C M --> O end subgraph Sources["Independent source coordinates"] S["_remote_source_paths_for_parent"] E["_expand_remote_parent_local_path"] S --> E E --> V["Same host, repo and ref; virtual sibling"] end C --> A["Package tree under safe alias"] A --> H["Cached inventory and content hash"] R --> S classDef changed stroke-dasharray: 5 5; class O,P,M,C,S changed;Legend: only reused cache bytes enter legacy admission; required replacements retain the existing fetch and activation path.
sequenceDiagram participant R as APMDependencyResolver participant D as Canonical ref drift participant F as Download callback participant L as legacy_plugin_compat participant V as Package validation participant A as Activation callback R->>D: _should_force_recheck(dep_ref) alt Required fetch R->>F: Fetch candidate using existing source authorization F-->>R: Candidate path else Reuse existing cache rect rgb(255, 247, 200) R->>L: cache_validation_callback L-->>R: Read-only admission or error end end R->>V: Validate before integration or activation Note over R,V: Legacy normalization follows cache admission alt Validated downloaded candidate R->>A: Publish candidate A-->>R: Live materialization path endBoth diagrams rendered successfully with
mmdc 11.15.0.Trade-offs
Benefits
../chains and aliased remote siblings retain their distinct source semantics.Validation
At exact head
ae3099de229141401b770af29c32dc0f63510f9b, the focused pytest invocation below completed:The complete local CI lint mirror passed: Ruff check and format across source/tests/architecture tooling, pylint R0801, auth boundaries, architecture boundaries, and the actual YAML I/O, portable-relative-path and 2100-line predicates. On macOS the grep predicates were evaluated with equivalent Python regexes because BSD grep has no
-P. This is local validation, not a claim that GitHub CI is green; after push the remote rollup exposed only the passing CLA check.Executed focused test command and red/mutation evidence
Run from the repository with a source-installed
apmbinary:tests/unit/install/test_install_copilot_user_instructions.py::TestCopilotUserInstructionsIntegration::test_project_scope_unaffectedrejects.copilotanywhere in an absolute deployed path, including this assigned checkout's.copilot/session-stateancestor. The correct.github/instructionsoutput existed. No unrelated test was edited.Scenario Evidence
tests/red_team/install/test_alias_path_escape.py::TestDownloadRejectsEscapingAlias;TestIntegrateRejectsEscapingAlias(regression-traps for #2900)tests/integration/test_required_lifecycle_state_machine.py::test_required_reinstall_is_byte_idempotent_across_durable_state(regression-trap for #2900)../base->../leafchain works with aliases and without them.tests/integration/test_transitive_chain_e2e.py::test_asymmetric_layout_anchors_on_declaring_pkgtests/test_apm_resolver.py::TestRemoteParentLocalPathFailClosed::test_remote_parent_same_repo_sibling_path_expands_to_remote_virtual_depand its two remote-path rejection controlstests/integration/test_required_lifecycle_state_machine.py::test_required_invalid_receiptless_legacy_cache_fails_with_recovery(nine cases);test_required_legacy_content_hash_upgrade_preserves_skills_and_convergestests/unit/test_alias_traversal.py::TestInstallPhaseSymlinkEscape::test_extended_prefix_alias_destinationHow to test
mkdir -p .work-2901, then run the focused command above; expect the alias, source and legacy-cache scenarios to pass..safe,safe.,foo..barormy-skill.v2; install twice and verify inventory/hash refer to that installed package, not the consumer..or..; expect an actionable failure without changing existing artifacts. Restore a safe alias; do not delete a path selected by the invalid alias.bash scripts/lint-architecture-boundaries.sh; expect the single-owner and cache-ordering guards to pass. Check the PR's actual required CI separately before merging.Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com