fix(install): allow local packages at --global scope; fix broken tests#937
fix(install): allow local packages at --global scope; fix broken tests#937stbenjam wants to merge 3 commits intomicrosoft:mainfrom
Conversation
Remove the validation that rejected local filesystem packages when using --global. There is no technical reason local paths cannot be installed at user scope -- the primitives are copied to ~/.claude/, ~/.gemini/, etc. the same way as at project scope. The rejection was added as a precaution against relative-path confusion, but absolute paths work fine and relative paths are resolved during parsing anyway. Also add test_global_scope_e2e.py to scripts/test-integration.sh so CI catches regressions in --global scope tests going forward. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
test_user_scope_skips_workspace_runtimes was missing the MCPServerOperations mock that the sibling test already had, causing it to hit the real registry and fail on the fake "test/server" name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
APM Review Panel VerdictDisposition: REQUEST_CHANGES (incomplete implementation -- three guards, one removed) Per-persona findingsPython Architect: The PR removes one guard in OO / class diagram classDiagram
direction LR
class _validate_and_add_packages_to_apm_yml {
<<IOBoundary>>
+packages list
+scope InstallScope
+logger InstallLogger
}
class LocalPackageSource {
<<Strategy>>
+acquire() Materialization
}
class download_callback {
<<Pure>>
+dep_ref DependencyReference
+modules_dir Path
}
class DependencyReference {
<<ValueObject>>
+is_local bool
+local_path str
+parse(dep_str) DependencyReference
}
class InstallScope {
<<Enum>>
PROJECT
USER
}
class _copy_local_package {
<<IOBoundary>>
+dep_ref DependencyReference
+install_path Path
+project_root Path
}
_validate_and_add_packages_to_apm_yml ..> DependencyReference : parses
_validate_and_add_packages_to_apm_yml ..> InstallScope : reads (guard REMOVED here)
LocalPackageSource ..> InstallScope : reads (guard STILL PRESENT)
download_callback ..> InstallScope : reads (guard STILL PRESENT)
LocalPackageSource ..> _copy_local_package : delegates
download_callback ..> _copy_local_package : delegates
class _validate_and_add_packages_to_apm_yml:::touched
class LocalPackageSource:::untouched
class download_callback:::untouched
classDef touched fill:#fff3b0,stroke:#d47600
classDef untouched fill:#ffcccc,stroke:#cc0000
note for LocalPackageSource "sources.py:141-153: still\nrejects USER scope with\n'not supported' diagnostic"
note for download_callback "resolve.py:136-142: still\nadds to callback_failures\nfor USER scope"
Execution flow diagram flowchart TD
A["apm install --global ./local-pkg"] --> B["_validate_and_add_packages_to_apm_yml\ncommands/install.py"]
B --> C["DependencyReference.parse(package)"]
C --> D{is_insecure?}
D -- No --> E["[BEFORE PR] is_local AND USER scope?\n-> validation_fail + reject"]
E -- "REMOVED by PR #937" --> F["[FS] Add to apm.yml\napm.yml updated with dep"]
D -- No --> F
F --> G["LocalPackageSource.acquire()\nsources.py:118"]
G --> H{scope is USER?}
H -- YES --> I["[STILL PRESENT] diagnostics.warn\n'local paths are not supported at user scope'\nreturn None -- package NOT deployed"]
H -- NO --> J["_copy_local_package()\nlocal_content.py:78"]
J --> K["[FS] shutil.copytree to apm_modules/\nIntegration pipeline runs"]
I --> L["apm.yml has entry,\nprimitives NOT deployed,\nexit code may be 0"]
style E fill:#ffcccc,stroke:#cc0000
style I fill:#ffcccc,stroke:#cc0000
style L fill:#ffeecc,stroke:#cc7700
```
**Design patterns**
- Used in this PR: none -- straight-line guard removal in a procedural validation function, appropriate for the scope.
- Pragmatic suggestion: none -- the current shape (sequential validation guards + collected outcomes) is the simplest correct design. The fix is to remove all three guards consistently, not to introduce a new pattern.
**Verdict**: The change is architecturally incomplete. `sources.py:141-153` and `resolve.py:136-142` enforce the same restriction that was removed from `commands/install.py`. The result is a broken half-state: the manifest records the dependency, the package is never deployed, and the user sees a contradictory diagnostic that still says "not supported at user scope."
---
**CLI Logging Expert**:
The removed validation correctly used `logger.validation_fail(package, reason)` -- the right pattern. No logging anti-patterns were introduced.
The live concern is `sources.py:141-153`, which still emits:
```
"Skipped local package '{dep_ref.local_path}' -- local paths are not supported at user scope (--global). Use a remote reference (owner/repo) instead."
This message will fire even after this PR merges, directly contradicting the PR's intent. If local packages are truly allowed, that diagnostic must be removed (or reworded) in the same PR. The E2E tests check DevX UX Expert: The intent -- aligning with However, the current implementation is UX-negative compared to the before state:
The fix must be complete (remove all three guards and verify One path-resolution note for when the fix is complete: Supply Chain Security Expert: The original restriction comment cited a real correctness risk: "relative paths resolve against cwd during validation but against $HOME during copy." Inspecting local = Path(dep_ref.local_path).expanduser()
if not local.is_absolute():
local = (project_root / local).resolve()At project scope, No new path traversal or token leakage surface is introduced. The Verdict: Low security risk, but the Auth Expert: Not activated -- no auth-relevant files changed; the PR removes a scope restriction ( OSS Growth Hacker: The goal -- But a half-baked relaxation that produces a manifest with an undeployed entry is a silent failure mode that would generate confused bug reports and erode trust. Fix it completely, then it deserves a Side-channel to CEO: once complete, this is a good "APM behaves like mature package managers" signal for the CHANGELOG. Not a standalone launch beat, but worth a clean release note line. CEO arbitrationThe PR's core intent is right -- Required actions before merge
Optional follow-ups
|
danielmeppiel
left a comment
There was a problem hiding this comment.
Great one but need adjustment per panel findings
Summary
I spotted some failing tests that don't run in CI.
--global. There is no technical reason local paths cannot be installed at user scope -- primitives are copied to~/.claude/,~/.gemini/, etc. the same way as at project scope.test_global_scope_e2e.pytoscripts/test-integration.shso CI catches regressions in--globalscope tests going forward. This file was never included in CI, so the broken test went undetected.test_user_scope_skips_workspace_runtimeswhich was missing theMCPServerOperationsmock. The test passed in CI by accident: no runtimes are installed in CI, soMCPIntegrator.install()returns early at the scope-filtering stage before reaching the registry validation. Locally, ifcodexis installed,target_runtimessurvives scope filtering and hits the real registry, which fails on the faketest/servername. The sibling test (test_project_scope_includes_all_runtimes) already had the correct mock.Test plan
pytest tests/unit tests/test_console.py tests/integration/test_global_scope_e2e.py-- 5459 passed, 0 failedtest_auto_bootstrap_creates_user_manifestnow passes (previously broken since local-package rejection was added)test_user_scope_skips_workspace_runtimesnow passes deterministically regardless of locally installed runtimesGenerated with Claude Code