feat: support allow-insecure HTTP dependencies#700
feat: support allow-insecure HTTP dependencies#700arika0093 wants to merge 20 commits intomicrosoft:mainfrom
Conversation
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR introduces an explicit opt-in flow for allowing HTTP (insecure) APM dependencies, including persisting that decision in apm.yml and preserving HTTP metadata during lockfile replays.
Changes:
- Add
--allow-insecureflag + globalallow-insecureconfig to gate installation ofhttp://dependencies. - Persist and replay HTTP dependency metadata (
is_insecure,allow_insecure) via manifest and lockfile. - Update docs and add unit tests covering HTTP parsing, lockfile round-trip, CLI behaviors, and cloning behavior.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_install_update.py | Adds lockfile replay/round-trip tests for insecure HTTP metadata. |
| tests/unit/test_install_command.py | Adds CLI tests for --allow-insecure and install-time security checks. |
| tests/unit/test_config_command.py | Adds config command + config function tests for allow-insecure. |
| tests/unit/test_canonicalization.py | Adds parsing/serialization tests for HTTP dependency handling. |
| tests/unit/test_auth_scoping.py | Ensures HTTP deps don’t fall back to SSH during clone attempts. |
| src/apm_cli/models/dependency/reference.py | Introduces is_insecure/allow_insecure, HTTP canonicalization, manifest serialization, and scheme-aware URLs. |
| src/apm_cli/drift.py | Preserves insecure scheme info during lockfile replay. |
| src/apm_cli/deps/lockfile.py | Persists is_insecure/allow_insecure in lockfile read/write and dependency refs. |
| src/apm_cli/deps/github_downloader.py | Adds HTTP-only clone path and URL building for insecure deps. |
| src/apm_cli/config.py | Adds get_allow_insecure / set_allow_insecure. |
| src/apm_cli/commands/install.py | Adds --allow-insecure, manifest writing behavior, and install-time HTTP enforcement. |
| src/apm_cli/commands/config.py | Generalizes config get/set to support allow-insecure. |
| src/apm_cli/commands/_helpers.py | Propagates HTTP/artifactory/local fields when computing install paths. |
| src/apm_cli/bundle/plugin_exporter.py | Propagates HTTP/artifactory/local fields for exported install paths. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Documents --allow-insecure in the command reference table. |
| docs/src/content/docs/reference/cli-commands.md | Documents --allow-insecure and new config key. |
| docs/src/content/docs/guides/dependencies.md | Documents HTTP dependency manifest format + caution guidance. |
101a07a to
3a28c8c
Compare
sergio-sisternes-epam
left a comment
There was a problem hiding this comment.
Solid feature -- good security model with dual opt-in (dep-level + CLI/config), thorough test coverage (30+ tests), clean lockfile round-trip, and well-written docs. Nice work!
One blocking issue: to_apm_yml_entry() hardcodes allow_insecure = True, which could silently escalate security posture. See inline comment.
Also: this PR silently fixes a bug where artifactory_prefix was missing from DependencyReference construction in _helpers.py, plugin_exporter.py, and lockfile.py -- directly related to #614. Consider referencing that issue in the PR description.
| entry["ref"] = self.reference | ||
| if self.alias: | ||
| entry["alias"] = self.alias | ||
| entry["allow_insecure"] = True |
There was a problem hiding this comment.
Blocking: This hardcodes allow_insecure = True for all HTTP deps, ignoring self.allow_insecure. If this method is reused in a code path that hasn't applied the opt-in check, it silently grants insecure access.
| entry["allow_insecure"] = True | |
| entry["allow_insecure"] = self.allow_insecure |
| ado_project=ado_project, | ||
| ado_repo=ado_repo, | ||
| artifactory_prefix=artifactory_prefix, | ||
| is_insecure=dependency_str.startswith("http://"), |
There was a problem hiding this comment.
Suggestion: startswith("http://") is case-sensitive, but URL schemes are case-insensitive per RFC 3986. HTTP://host/repo would bypass insecure detection.
| is_insecure=dependency_str.startswith("http://"), | |
| is_insecure=urllib.parse.urlparse(dependency_str).scheme.lower() == "http", |
| return build_ssh_url(host, repo_ref) | ||
| elif is_github and github_token: | ||
| # Only send GitHub tokens to GitHub hosts | ||
| # # Only send GitHub tokens to GitHub hosts |
There was a problem hiding this comment.
Nit: Double # — typo from the refactor commit.
| # # Only send GitHub tokens to GitHub hosts | |
| # Only send GitHub tokens to GitHub hosts |
| sub_path = entry.get("path") | ||
| ref_override = entry.get("ref") | ||
| alias_override = entry.get("alias") | ||
| allow_insecure = bool(entry.get("allow_insecure", False)) |
There was a problem hiding this comment.
Suggestion: bool(entry.get("allow_insecure", False)) treats any non-empty string (e.g., "false") as True. Since this is a security-relevant field, consider validating the type explicitly:
| allow_insecure = bool(entry.get("allow_insecure", False)) | |
| allow_insecure = entry.get("allow_insecure", False) | |
| if not isinstance(allow_insecure, bool): | |
| raise ValueError("'allow_insecure' field must be a boolean") |
|
@sergio-sisternes-epam On #614: this PR now explicitly passes |
sergio-sisternes-epam
left a comment
There was a problem hiding this comment.
All findings addressed — nice work, @arika0093! Each fix is clean and well-tested.
Thanks for the clarification on #614 — you're right that the artifactory_prefix propagation fix here is a partial improvement but the core _parse_artifactory_base_url() issue remains separate.
LGTM!
danielmeppiel
left a comment
There was a problem hiding this comment.
First — thanks for tackling this, @arika0093. The use case is real (internal mirrors without HTTPS exist in the wild), the dual opt-in design (per-dep allow_insecure: true + CLI flag) is the right shape, and the test coverage is thorough. Sergio's APPROVE isn't wrong on the implementation quality.
I'm requesting changes from a security/UX lens because APM is a supply-chain tool, and a few aspects of the current design create footguns that I don't think we can ship to the broader community. None of these are deal-breakers — they're scope tightening. If we land them, this becomes a feature I'd be proud to ship.
Blockers
1. Drop apm config set allow-insecure true (the persistent global config)
This is the biggest single risk in the PR and, IMO, the only true blocker.
Per-invocation --allow-insecure is the right friction ceiling. A persistent global setting:
- Means a developer enables it once for one internal project and then forever silently allows HTTP for every subsequent
apm installon that machine, including in unrelated repos. - Turns shared CI runners into a network-MITM surface for any repo that lands
allow_insecure: trueinapm.yml— the runner doesn't need to opt in per-job, the maintainer who set the config months ago already did. - Is invisible at install time. There's no per-install reminder. Users forget they set it.
- Converts a per-action security decision into ambient permission, which is the textbook supply-chain footgun.
The per-invocation flag is the correct model. It's friction, but the friction is the security feature: every HTTP install requires an active, conscious choice. Please drop the apm config set allow-insecure path entirely (and the corresponding get_allow_insecure / set_allow_insecure config functions).
2. Be loud at install time about which URLs are insecure
Today, the user only sees output about HTTP when --allow-insecure is missing (the error path). On the success path there's no equivalent loud diagnostic listing exactly which URLs are about to be fetched over an unauthenticated channel.
Compare to pip's behavior on insecure indexes: a clear warning per fetch. APM should do the same.
Concretely:
- Print one
[!]line per HTTP dependency at install-prepare time, with the full URL:[!] Fetching insecurely (no transport auth): http://internal.example.com/team/foo - Make this print whether or not
--allow-insecurewas passed (i.e. the warning is informational; the flag is the gate). - This is what makes Alice notice when Bob's
apm.ymlgrew an unexpected HTTP entry betweengit pulls.
3. Transitive HTTP dependencies need a separate gate
This is the most subtle one and the one I'd most appreciate your thinking on. Scenario:
- Bob adds a top-level dep
http://internal.bob.example/foo/barwithallow_insecure: trueto the project'sapm.yml. Alice can see this in the diff and consents by passing--allow-insecure. - But Bob's package itself has an
apm.ymlthat pulls a transitive dephttp://attacker-controlled.example/lib/xover HTTP, also withallow_insecure: true. - When Alice runs
apm install --allow-insecure, that transitive dep gets fetched too. Alice never saw it in the project's ownapm.yml. Her consent at the root extends silently to a graph she didn't review.
The lockfile makes this auditable after it's pinned, but the first install (and any update) doesn't surface this distinction.
Suggested approach (open to alternatives):
- Track per-dep whether the HTTP+
allow_insecurechoice originated in the rootapm.ymlor in a transitive dep. - For transitive HTTP, require an additional explicit acknowledgment: either a separate
--allow-insecure-transitiveflag, or an interactive prompt listing the transitive HTTP URLs and their introducing parent, with--yesto skip in CI. - At minimum, the per-URL warning from #2 should annotate
(transitive, introduced by <parent-dep>)so users can see it.
Without this, the dual-opt-in only protects the immediate dep — the transitive surface is a single root flag away from being silently expanded.
Non-blocking but please consider
- Threat-model docs: the new HTTP section in
dependencies.mddocuments the mechanism but not the why. A short paragraph explaining that HTTP has no transport auth (so an MITM can substitute the package contents — the SHA in the lockfile is itself MITM-able when fetched over HTTP) would help users make informed choices instead of copy-pasting the example. - Audit affordance: there's no way to ask "list every insecure dep in my dependency graph" before running install. With the lockfile already preserving
is_insecure, anapm deps list --insecure(orapm install --dry-runlisting them) would be cheap and high-value. Could be a follow-up. - CHANGELOG entry: this is a security-sensitive feature. The CHANGELOG entry should be explicit that this introduces an opt-in HTTP path with the threat-model implications, not just "feat: support http deps."
- Hostname allowlist (future, not now): a future iteration could let
apm.ymldeclare aninsecure_hosts: [...]allowlist so that HTTP is only permitted from explicitly-named hosts. Not for this PR.
What I'd merge
If you land #1 (drop global config) and #2 (loud per-URL output), I'd merge. #3 (transitive gating) is the one I feel strongest about from a supply-chain perspective; if you'd prefer to defer it, I'd accept that as a follow-up issue only if the per-URL warning from #2 explicitly annotates transitive HTTP deps so the information is at least visible.
If you'd rather not drop the global config, I'd respectfully decline this PR and recommend users set up HTTPS on their internal mirrors. APM is a supply-chain tool and persistent ambient "allow insecure" is incompatible with that role.
Thanks again for the thoughtful design and the thorough tests — this is salvageable and worth landing well.
|
Thanks for the thoughtful review. I think your concerns are reasonable, especially given that APM is a supply-chain tool. I agree on the first two blockers:
On transitive HTTP dependencies: I agree this is the trickiest part. A root-level opt-in should not silently bless an unreviewed transitive graph. After thinking more about it, I do not think a simple For this PR, my plan is:
I think this is a better narrow fix for the security concern raised here, while also fitting the common internal-repository use case more naturally than a boolean transitive flag. I will also update the docs and changelog accordingly. The audit affordance ( Thanks again for the careful review. |
- docs(dependencies): pin BREAKING-change caution panel to APM 0.8.13
(per maintainer review comment)
- transport_selection: collapse explicit `http://` URLs into the plain-HTTPS
branch so TransportAttempt.scheme stays in {ssh, https} and the downloader
contract is consistent until #700 lands a first-class HTTP transport
- github_downloader: gate the [!] "Protocol fallback" warning on actual
scheme change (ssh<->https) rather than label change, so an auth downgrade
inside a single protocol is not misreported as a protocol switch
- pipeline / request / context / commands: switch
`allow_protocol_fallback` to `Optional[bool] = None` end-to-end so
programmatic callers (non-CLI) keep the documented "None => read
APM_ALLOW_PROTOCOL_FALLBACK env" behavior
- test_auth_scoping: ASCII-only docstring (replace one stray Unicode arrow)
All 4061 unit tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… (#779) * feat(transport): TransportSelector + strict-by-default transport (#778) Implements the core decision engine for issue #778 'transport selection v1'. Strict-by-default semantics replace today's silent cross-protocol fallback: explicit ssh:// and https:// dependencies no longer downgrade to a different protocol, and shorthand (owner/repo) consults git insteadOf rewrites before defaulting to HTTPS. This commit ships Waves 1+2 of the transport-selection plan (per session plan): - new module src/apm_cli/deps/transport_selection.py with ProtocolPreference, TransportAttempt/TransportPlan dataclasses, GitConfigInsteadOfResolver, and TransportSelector that returns a typed, strict-by-default plan - DependencyReference grows explicit_scheme so the selector can distinguish user-stated transport from shorthand - _clone_with_fallback in github_downloader.py now iterates the selector plan; per-attempt URL building stays in the orchestrator - InstallContext / InstallRequest / pipeline / Service threaded with protocol_pref + allow_protocol_fallback so CLI args reach the downloader - apm install gains --ssh / --https (mutually exclusive) and --allow-protocol-fallback flags; honours APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars - two pre-existing tests in test_auth_scoping.py asserted the legacy permissive chain; updated to assert the new strict contract and added a coverage test for the allow_fallback escape hatch Tests: 4029 unit tests pass. Test matrix + integration tests + docs land in subsequent commits per Waves 3-5. Refs #778, #328, #661 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(transport): wave-3 unit + integration matrix; fix per-attempt clone env Wave 2 panel gate (code-review subagent) flagged that GitHubPackageDownloader._clone_with_fallback decided the clone env (locked-down vs relaxed) ONCE per dependency from has_token. Under allow_fallback=True the plan can mix attempts of different use_token values, so SSH and plain-HTTPS attempts in a mixed chain were running with GIT_ASKPASS=echo + GIT_CONFIG_GLOBAL=/dev/null + GIT_CONFIG_NOSYSTEM=1, breaking ssh-agent passphrase prompts and git credential helpers. Fix the env per attempt; address an adjacent contract bug; add tests. * github_downloader._clone_with_fallback: replace the per-dep clone_env with a per-attempt _env_for() helper so only token-bearing attempts get the locked-down env. * github_downloader._build_repo_url: treat token="" as an explicit "no token" sentinel so plain-HTTPS attempts in a mixed chain genuinely run without embedded credentials, letting credential helpers (gh auth, Keychain) supply auth. Orchestrator passes "" instead of None for use_token=False attempts. * transport_selection.GitConfigInsteadOfResolver: wrap the lazy insteadOf-rewrite cache in a threading.Lock so parallel downloads can't double-populate. Tests: * tests/unit/test_transport_selection.py (NEW, 30 tests): 14-row selection matrix (explicit-strict, shorthand+insteadOf, shorthand defaults, CLI prefs, allow_fallback chain, env helpers); resolver caching; "must use normal env" contract. * tests/unit/test_auth_scoping.py: new test_allow_fallback_env_is_per_attempt_not_per_dep regression asserts auth-HTTPS gets locked-down env, SSH and plain-HTTPS get relaxed env, and plain-HTTPS does not embed the token in the URL. * tests/integration/test_transport_selection_integration.py (NEW, 7 tests): 2 always-on cases (public shorthand HTTPS; explicit https:// strict); 5 SSH-required cases (explicit ssh:// strict, bad-host no-fallback, insteadOf override, APM_GIT_PROTOCOL=ssh env, allow_fallback rescue). Gated on APM_RUN_INTEGRATION_TESTS=1; SSH cases auto-skip if no key. * tests/fixtures/gitconfig_insteadof_to_ssh (NEW): minimal gitconfig used by the integration test for the insteadOf-honored case. * scripts/test-integration.sh: added "Transport Selection" block so the integration suite runs in CI. Full unit suite: 4061 passed (was 4029; +32 net new tests). Refs #778, #661, #328 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(transport): document strict-by-default transport selection (#778) Update the four user-facing surfaces affected by the new TransportSelector contract: * docs/src/content/docs/guides/dependencies.md: - New "Transport selection (SSH vs HTTPS)" section: breaking-change callout with the rescue env var, selection matrix, insteadOf example, --ssh / --https / APM_GIT_PROTOCOL overrides, and the --allow-protocol-fallback escape hatch. - Soften the existing "Custom ports preserved" sentence (cross-protocol retries are now opt-in). - Update the "Other Git Hosts" SSH bullet: SSH is no longer a silent fallback; point at explicit URLs or insteadOf. * docs/src/content/docs/getting-started/authentication.md: - Rewrite the "SSH connection hangs" troubleshooting entry: remove the now-incorrect "tries SSH then falls back to HTTPS" framing. - New "Choosing transport (SSH vs HTTPS)" section with a pointer to dependencies.md for the full transport contract. * docs/src/content/docs/reference/cli-commands.md: - Document --ssh / --https / --allow-protocol-fallback on apm install, plus APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars. * packages/apm-guide/.apm/skills/apm-usage/dependencies.md (Rule 4 mirror): - Same transport contract in skill-resource voice with three runnable snippets and a selection matrix. CHANGELOG: scope new entries to `apm install` only (there is no `apm add` command in the codebase). Refs #778 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: revert accidental uv.lock churn (no dependency change in this PR) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs+changelog: address copilot review feedback (#779) Two findings from the Copilot reviewer on PR #779: 1. Non-ASCII em dash introduced by this PR in the modified `Fields:` line of guides/dependencies.md: replace with `--`. The other non-ASCII chars Copilot flagged in the file (lines ~150-160 of the "nested groups" warning block) are pre-existing and out of scope for this PR. 2. CHANGELOG entries for the new transport-selection feature were too long and bundled multiple concerns into one bullet. Split into one tighter BREAKING entry plus two single-purpose Added entries (initial-protocol flags; fallback escape hatch). Each ends in `(#778)`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #779 review feedback - docs(dependencies): pin BREAKING-change caution panel to APM 0.8.13 (per maintainer review comment) - transport_selection: collapse explicit `http://` URLs into the plain-HTTPS branch so TransportAttempt.scheme stays in {ssh, https} and the downloader contract is consistent until #700 lands a first-class HTTP transport - github_downloader: gate the [!] "Protocol fallback" warning on actual scheme change (ssh<->https) rather than label change, so an auth downgrade inside a single protocol is not misreported as a protocol switch - pipeline / request / context / commands: switch `allow_protocol_fallback` to `Optional[bool] = None` end-to-end so programmatic callers (non-CLI) keep the documented "None => read APM_ALLOW_PROTOCOL_FALLBACK env" behavior - test_auth_scoping: ASCII-only docstring (replace one stray Unicode arrow) All 4061 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ade (#780) (#781) * feat(transport): TransportSelector + strict-by-default transport (#778) Implements the core decision engine for issue #778 'transport selection v1'. Strict-by-default semantics replace today's silent cross-protocol fallback: explicit ssh:// and https:// dependencies no longer downgrade to a different protocol, and shorthand (owner/repo) consults git insteadOf rewrites before defaulting to HTTPS. This commit ships Waves 1+2 of the transport-selection plan (per session plan): - new module src/apm_cli/deps/transport_selection.py with ProtocolPreference, TransportAttempt/TransportPlan dataclasses, GitConfigInsteadOfResolver, and TransportSelector that returns a typed, strict-by-default plan - DependencyReference grows explicit_scheme so the selector can distinguish user-stated transport from shorthand - _clone_with_fallback in github_downloader.py now iterates the selector plan; per-attempt URL building stays in the orchestrator - InstallContext / InstallRequest / pipeline / Service threaded with protocol_pref + allow_protocol_fallback so CLI args reach the downloader - apm install gains --ssh / --https (mutually exclusive) and --allow-protocol-fallback flags; honours APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars - two pre-existing tests in test_auth_scoping.py asserted the legacy permissive chain; updated to assert the new strict contract and added a coverage test for the allow_fallback escape hatch Tests: 4029 unit tests pass. Test matrix + integration tests + docs land in subsequent commits per Waves 3-5. Refs #778, #328, #661 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(transport): wave-3 unit + integration matrix; fix per-attempt clone env Wave 2 panel gate (code-review subagent) flagged that GitHubPackageDownloader._clone_with_fallback decided the clone env (locked-down vs relaxed) ONCE per dependency from has_token. Under allow_fallback=True the plan can mix attempts of different use_token values, so SSH and plain-HTTPS attempts in a mixed chain were running with GIT_ASKPASS=echo + GIT_CONFIG_GLOBAL=/dev/null + GIT_CONFIG_NOSYSTEM=1, breaking ssh-agent passphrase prompts and git credential helpers. Fix the env per attempt; address an adjacent contract bug; add tests. * github_downloader._clone_with_fallback: replace the per-dep clone_env with a per-attempt _env_for() helper so only token-bearing attempts get the locked-down env. * github_downloader._build_repo_url: treat token="" as an explicit "no token" sentinel so plain-HTTPS attempts in a mixed chain genuinely run without embedded credentials, letting credential helpers (gh auth, Keychain) supply auth. Orchestrator passes "" instead of None for use_token=False attempts. * transport_selection.GitConfigInsteadOfResolver: wrap the lazy insteadOf-rewrite cache in a threading.Lock so parallel downloads can't double-populate. Tests: * tests/unit/test_transport_selection.py (NEW, 30 tests): 14-row selection matrix (explicit-strict, shorthand+insteadOf, shorthand defaults, CLI prefs, allow_fallback chain, env helpers); resolver caching; "must use normal env" contract. * tests/unit/test_auth_scoping.py: new test_allow_fallback_env_is_per_attempt_not_per_dep regression asserts auth-HTTPS gets locked-down env, SSH and plain-HTTPS get relaxed env, and plain-HTTPS does not embed the token in the URL. * tests/integration/test_transport_selection_integration.py (NEW, 7 tests): 2 always-on cases (public shorthand HTTPS; explicit https:// strict); 5 SSH-required cases (explicit ssh:// strict, bad-host no-fallback, insteadOf override, APM_GIT_PROTOCOL=ssh env, allow_fallback rescue). Gated on APM_RUN_INTEGRATION_TESTS=1; SSH cases auto-skip if no key. * tests/fixtures/gitconfig_insteadof_to_ssh (NEW): minimal gitconfig used by the integration test for the insteadOf-honored case. * scripts/test-integration.sh: added "Transport Selection" block so the integration suite runs in CI. Full unit suite: 4061 passed (was 4029; +32 net new tests). Refs #778, #661, #328 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(transport): document strict-by-default transport selection (#778) Update the four user-facing surfaces affected by the new TransportSelector contract: * docs/src/content/docs/guides/dependencies.md: - New "Transport selection (SSH vs HTTPS)" section: breaking-change callout with the rescue env var, selection matrix, insteadOf example, --ssh / --https / APM_GIT_PROTOCOL overrides, and the --allow-protocol-fallback escape hatch. - Soften the existing "Custom ports preserved" sentence (cross-protocol retries are now opt-in). - Update the "Other Git Hosts" SSH bullet: SSH is no longer a silent fallback; point at explicit URLs or insteadOf. * docs/src/content/docs/getting-started/authentication.md: - Rewrite the "SSH connection hangs" troubleshooting entry: remove the now-incorrect "tries SSH then falls back to HTTPS" framing. - New "Choosing transport (SSH vs HTTPS)" section with a pointer to dependencies.md for the full transport contract. * docs/src/content/docs/reference/cli-commands.md: - Document --ssh / --https / --allow-protocol-fallback on apm install, plus APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars. * packages/apm-guide/.apm/skills/apm-usage/dependencies.md (Rule 4 mirror): - Same transport contract in skill-resource voice with three runnable snippets and a selection matrix. CHANGELOG: scope new entries to `apm install` only (there is no `apm add` command in the codebase). Refs #778 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: revert accidental uv.lock churn (no dependency change in this PR) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs+changelog: address copilot review feedback (#779) Two findings from the Copilot reviewer on PR #779: 1. Non-ASCII em dash introduced by this PR in the modified `Fields:` line of guides/dependencies.md: replace with `--`. The other non-ASCII chars Copilot flagged in the file (lines ~150-160 of the "nested groups" warning block) are pre-existing and out of scope for this PR. 2. CHANGELOG entries for the new transport-selection feature were too long and bundled multiple concerns into one bullet. Split into one tighter BREAKING entry plus two single-purpose Added entries (initial-protocol flags; fallback escape hatch). Each ends in `(#778)`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #779 review feedback - docs(dependencies): pin BREAKING-change caution panel to APM 0.8.13 (per maintainer review comment) - transport_selection: collapse explicit `http://` URLs into the plain-HTTPS branch so TransportAttempt.scheme stays in {ssh, https} and the downloader contract is consistent until #700 lands a first-class HTTP transport - github_downloader: gate the [!] "Protocol fallback" warning on actual scheme change (ssh<->https) rather than label change, so an auth downgrade inside a single protocol is not misreported as a protocol switch - pipeline / request / context / commands: switch `allow_protocol_fallback` to `Optional[bool] = None` end-to-end so programmatic callers (non-CLI) keep the documented "None => read APM_ALLOW_PROTOCOL_FALLBACK env" behavior - test_auth_scoping: ASCII-only docstring (replace one stray Unicode arrow) All 4061 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(install): MARKETPLACE_PLUGIN beats HOOK_PACKAGE in detection cascade (#780) obra/superpowers (and any Claude Code plugin shipping both hooks/*.json and agents/skills/commands) was misclassified as HOOK_PACKAGE because the cascade in detect_package_type() checked _has_hook_json BEFORE the plugin-evidence branch. The plugin synthesizer (_map_plugin_artifacts) already maps hooks alongside agents/skills/commands, so MARKETPLACE_PLUGIN is a strict superset -- swapping the order means hooks still install, plus everything else that was being silently dropped. Three deliverables: A) Surgical detection fix: reorder cascade so MARKETPLACE_PLUGIN is checked before HOOK_PACKAGE. Refactored to use a new gather_detection_evidence() helper + DetectionEvidence dataclass so observability code (warnings, summaries) can reuse the same scan without breaking the detect_package_type() public signature. B) Observability: - Add HOOK_PACKAGE to the package-type label table (it was missing entirely -- the silent classification path). - Update MARKETPLACE_PLUGIN label to mention plugin.json OR agents/skills/commands (matches the cascade behaviour). - New CommandLogger.package_type_warn() at default visibility. - New _warn_if_classification_near_miss() helper fires when a HOOK_PACKAGE classification disagrees with directory contents (catches near-misses the order swap cannot, e.g. .claude-plugin/ dir without plugin.json). - Wired at both materialization sites (local + cached). C) Architectural follow-up tracked in plan; will file as separate issue after merge for a Visitor / Format-Discovery refactor. Tests: 17 detection tests + 9 sources-observability tests + 70 command-logger tests pass. Full unit suite (4180 tests) green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #781 review: broaden detect_package_type with .claude-plugin/ evidence Per Copilot review on PR #781: the near-miss warning helper had dead code paths once the cascade was reordered (a HOOK_PACKAGE classification implies plugin_dirs_present is empty -- otherwise it would be MARKETPLACE_PLUGIN). Two principled options were offered: simplify the helper, or broaden detection so the invariants stay consistent. Chose the latter -- it removes the inconsistency at the source. Changes: - Add .claude-plugin/ as first-class plugin evidence in DetectionEvidence (new has_claude_plugin_dir field) and in has_plugin_evidence. A Claude Code plugin without a plugin.json (manifest-less) now classifies as MARKETPLACE_PLUGIN; normalize_plugin_directory already handles the missing-manifest case (derives name from directory). - Drop _warn_if_classification_near_miss helper, its two call sites, CommandLogger.package_type_warn method, and the corresponding tests. All scenarios it covered are now handled by the cascade itself. - Add regression test test_claude_plugin_dir_alone_is_plugin_evidence asserting that .claude-plugin/ + hooks/ classifies as MARKETPLACE_PLUGIN with plugin_json_path=None (matched via directory evidence alone). - Extend test_obra_superpowers_evidence to assert has_claude_plugin_dir. Verified: 4175 unit tests pass (excluding the one pre-existing unrelated failure documented on main). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dbb6977 to
1796653
Compare
danielmeppiel
left a comment
There was a problem hiding this comment.
APM Review Panel -- PR #700
[x] Verdict: REQUEST CHANGES -- one CRITICAL credential-leak finding plus a converging architectural/security issue around HTTP identity. The dual-opt-in design is sound and Sergio's earlier blocker is correctly fixed; the items below are what stand between this and merge.
@arika0093 -- this is a substantial, security-sensitive contribution and the design instincts are right. Five reviewers (python-architect, cli-logging-expert, devx-ux-expert, supply-chain-security-expert; growth-hacker side-channel) converged on a tight set of must-fixes. Thank you for the depth here -- 1338 lines + 30+ tests on a class of feature most projects ship sloppily.
What's excellent
- Dual opt-in (manifest
allow_insecure: true+ CLI--allow-insecure) is the right mental model. Matches npmoverrides+--legacy-peer-deps. Neither alone is sufficient -- correct. - Persistent global config dropped per earlier review feedback. Right call; every install run should be a conscious act.
--allow-insecure-hostfor transitive deps with same-host auto-propagation for direct deps is well-designed. Good FQDN validation prevents flag injection.parse_from_dict()validatesallow_insecureis boolean -- prevents YAML type confusion.- Doc surface coverage (cli-commands.md, dependencies.md, apm-guide skill resources, CHANGELOG) is exactly the discipline this codebase demands.
Required changes (must land in this PR)
-
[x] CRITICAL -- HTTP clone leaks credentials in plaintext.
github_downloader.py_env_for(use_token=False)stripsGIT_ASKPASSandGIT_CONFIG_NOSYSTEMfor HTTP attempts, re-enabling system git credential helpers (macOS Keychain, Windows Credential Manager,gh auth). When git cloneshttp://internal.example.com/owner/repo, credential helpers may resolve stored tokens for that host and embed them as plaintextAuthorizationheaders. The existing testtest_insecure_http_dep_is_strict_by_defaultasserts"GIT_ASKPASS" not in env_used-- that test is verifying the vulnerability.Fix: HTTP attempts must actively block credential resolution:
if attempt.scheme == "http": env = dict(self.git_env) env['GIT_ASKPASS'] = 'echo' env['GIT_TERMINAL_PROMPT'] = '0' env['GIT_CONFIG_NOSYSTEM'] = '1' # Suppress credential helpers explicitly env['GIT_CONFIG_COUNT'] = '1' env['GIT_CONFIG_KEY_0'] = 'credential.helper' env['GIT_CONFIG_VALUE_0'] = '' return env
Update the test to assert credential-helper suppression for HTTP rather than its absence.
-
[x] BLOCKER -- HTTP/HTTPS identity collision enables transport downgrade.
get_unique_key()andget_identity()are scheme-blind, anddrift.pydocuments "Source/host/scheme changes -- not detected." That meanshttps://gitlab.com/team/rulesandhttp://gitlab.com/team/rulescollide as the same dep. An attacker submitting a PR that flips the scheme + addsallow_insecure: trueproduces zero lockfile drift signal. Combined with finding #1, this is a clean transport-downgrade exploit chain.Fix: Either (a)
get_identity()includes scheme whenis_insecure=True, or (b)drift.pytreats ais_insecureflip as drift requiring--update. Option (b) is the smaller change and preserves identity stability for non-HTTP deps. -
[x] BLOCKER -- Inverted import:
install/phases/resolve.pyimports fromcommands/install.py.resolve.py:297-300:from apm_cli.commands.install import ( _check_insecure_dependencies, _collect_insecure_dependency_infos, _guard_transitive_insecure_dependencies, _warn_insecure_dependencies, )
Domain code (
install/phases/) must never import from CLI command code. Extract the ~200 lines of insecure-policy logic +_InsecureDependencyInfodataclass into a newsrc/apm_cli/install/insecure_policy.py. Bothcommands/install.pyandinstall/phases/resolve.pythen import from the new module. Pure move, zero logic change. -
[x] BLOCKER --
drift.pypropagatesis_insecurebut notallow_insecureduring lockfile replay. Lines +250-252:build_download_ref()copiesis_insecure=Truebut dropsallow_insecure. The replayedDependencyReferencehasis_insecure=True, allow_insecure=False, which_check_insecure_dependencies()rejects. Lockfile replays of HTTP deps are broken (or worse, bypass the check via a different code path).if getattr(locked_dep, "is_insecure", False) is True: overrides["is_insecure"] = True overrides["allow_insecure"] = getattr(locked_dep, "allow_insecure", False)
-
[x] BLOCKER --
to_canonical()no longer stripshttp://, breaking the canonical identity contract. Architect + DevX both flagged this. The docstring update silently removes "no https://" and HTTP deps now producehttp://host/owner/repowhile HTTPS still strips. Every consumer that doesdep.to_canonical().split("/")or assumes no://will silently diverge for HTTP deps. Fix: keepto_canonical()scheme-free. The transport-aware string already exists asto_apm_yml_entry()-- use that for serialization paths and any surface that needs the scheme. -
[!] HIGH -- Add
LockedDependency.to_dependency_ref()factory. Three sites reconstructDependencyReferencefromLockedDependency:_helpers.py:138,plugin_exporter.py:399,lockfile.py:365. This PR already had to fix the missingartifactory_prefix(#614 root cause) AND addis_insecure/allow_insecureat all three. Per the "abstract when 3+ call sites" rule, add a factory method that maps every field once. This is the structural fix Sergio asked about -- the artifactory_prefix patch is a band-aid without it. -
[!] HIGH -- Unify the three competing error messages into one canonical recipe. Today a user sees three different errors depending on which gate failed:
- validation-time ("Pass
--allow-insecure"), - install-time/manifest-missing ("set allow_insecure: true"),
- install-time/flag-missing ("Pass
--allow-insecure").
None give the complete recipe. Collapse to one message that always names both requirements:
[x] http://my-server.example.com/owner/repo -- HTTP dependency (no transport encryption) To install: 1. Set allow_insecure: true on the dep in apm.yml 2. Pass --allow-insecure to apm installAnd use the full URL (with scheme) in every error -- the user needs to see why it's insecure.
- validation-time ("Pass
-
[!] HIGH -- Update
docs/src/content/docs/enterprise/security.md. This PR introduces an entirely new transport surface and threat model; per the repo rule ("If a code change weakens or contradicts any guarantee in security.md, the doc must be updated in the same PR"), security.md needs an "HTTP (insecure) dependencies" section covering: whatallow_insecuredoes/doesn't protect against, MITM impact on content-hash provenance, credential-helper isolation requirement, the transitive-host boundary model. -
[!] HIGH -- Logging routing: dead
else: _rich_*()fallback branches._warn_insecure_dependencies,_guard_transitive_insecure_dependencies,_check_insecure_dependenciesrepeatif logger: logger.X() else: _rich_X(...)7 times. Callers always have a logger; the else branches are dead code and bypassDiagnosticCollectorif ever triggered. Makeloggera required positional arg; drop every fallback branch.
Polish (same PR if cheap; follow-up otherwise)
-
[i] Library code calls
sys.exit(1)._check_insecure_dependenciesand_guard_transitive_insecure_dependenciesshould raise an exception; CLI layer catches and exits. Improves testability and reuse. -
[i]
_format_insecure_dependency_warninguses jargon ("no transport auth"). Reword:[!] Insecure HTTP fetch (unencrypted): http://... -
[i] Transitive-block error is a policy lecture. Drop the middle sentence; lead with the command. Move the "why" to docs.
-
[i]
apm deps list --insecurecolumn label "HTTP" misleads (values aredirect/via <parent>). Rename toOrigin. -
[i] Unrelated cleanups (config.py refactor,
_setup_git_environmentSSH cleanup) belong in separate commits for bisectability.
Worth a follow-up issue (don't block this PR)
APM_ALLOW_INSECURE=1env var counterpart for CI ergonomics (mirrorAPM_ALLOW_PROTOCOL_FALLBACK=1pattern). Not adding this is fine for v1; tracking issue acceptable.- Drift-detection enhancement: warn on first-time HTTP install without lockfile SHA pin (defense-in-depth against the hash-circular-trust limitation).
Merge call
This is solid first-PR work on a security-sensitive feature -- the dual-opt-in design and host scoping are correct. The blockers are concentrated in three areas: credential leakage (#1), identity model (#2, #5), and architectural cleanup (#3, #4, #6). Items #1-#5 are the merge-blockers; #6-#9 should land in the same PR; the rest is polish or follow-up. Please address #1-#9 in this PR and re-request review.
Growth angle
- Real adoption blocker unlocked: corporate Gitea/Gogs/Artifactory-on-HTTP and air-gapped enterprise users -- the segment that quietly worked around APM's "install from anywhere" claim with manual
git clone. Worth a release-notes call-out. - First-time NONE-association contributor shipping 1338 lines + 30+ tests on a security feature is the contributor signal worth amplifying once this lands. Suggested release-notes line: "APM now supports private HTTP git hosts with explicit dual-gate opt-in (manifest + CLI). Insecure by default? Never. Insecure when you need it, with a clear audit trail." Repostable hook: "APM doesn't block your infra -- it makes insecure choices visible."
- Once shipped, security.md becomes the authoritative reference for the threat model -- making the doc update in #8 above doubly load-bearing.
Reviewed via the apm-review-panel skill: python-architect, cli-logging-expert, devx-ux-expert, supply-chain-security-expert; arbitrated by apm-ceo with oss-growth-hacker side-channel.
1796653 to
803a126
Compare
…split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… 14 stack split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ck split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tack split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… stack split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…4 split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m 14 stack split) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ew item 1) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…em 2) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…item 3) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…em 4) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… item 5) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eview item 6) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…view item 9) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m 10) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b2a0a78 to
b8a5c5f
Compare
|
Thanks for the detailed review. I addressed all requested changes from this review, rewrote the stack so the commits are easier to map back to the review items, and kept the split granularity intact. Review item mapping:
For review item 14:
At this point, all review items from this review have been addressed in the branch. |
Description
This PR adds explicit support for HTTP (insecure) APM dependencies behind an opt-in flow, and makes lockfile replays preserve that HTTP source information.
When adding a package, you can explicitly allow HTTP dependencies by providing the
--allow-insecureflag.When restoring packages, you also need to provide the
--allow-insecureflag unless the global config is enabled. If there are HTTP URLs in the dependency chain, it will error without--allow-insecureorapm config set allow-insecure true.The
apm.ymlfile will record it in the following format:HTTP dependencies require two explicit signals:
allow_insecure: trueon the dependency entry inapm.ymlapm install --allow-insecureorapm config set allow-insecure trueThe global configuration only removes the need to pass
--allow-insecureon the CLI. It does not remove theallow_insecure: truerequirement inapm.yml.Fixes #636
TODO
--allow-insecureflag when adding dependencies--allow-insecureflag when installing dependenciesallow_insecure: truefield inapm.ymland lockfileallow-insecureglobal configType of change
Testing