Add GitHub Enterprise (GHE) hostname support to dependency parsing#8
Add GitHub Enterprise (GHE) hostname support to dependency parsing#8danielmeppiel merged 4 commits intomicrosoft:mainfrom
Conversation
remove all hard coded github.com and replace with parameterised
GPT5 mocking the docs... :)
There was a problem hiding this comment.
Pull Request Overview
This PR adds GitHub Enterprise (GHE) hostname support to dependency parsing so apm can correctly detect and normalize repository references hosted on self-managed GitHub instances.
Key Changes:
- Introduces a new
github_hostutility module that centralizes GitHub hostname validation and URL construction - Updates dependency parsing to accept custom GHE hostnames (e.g.,
orgname.ghe.com) in addition togithub.com - Adds environment variable support (
GITHUB_HOST,APM_GITHUB_HOSTS) for configuring custom GitHub instances - Updates all tests to use the new hostname utilities for consistency
Reviewed Changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/apm_cli/utils/github_host.py | New utility module providing hostname validation, URL construction, and token sanitization for GitHub and GHE instances |
| src/apm_cli/models/apm_package.py | Updates DependencyReference to support GHE hostnames, storing host information and using new utilities for validation |
| src/apm_cli/deps/github_downloader.py | Refactors to use github_host utilities for URL construction and token sanitization, adds host tracking for enterprise repos |
| src/apm_cli/adapters/client/copilot.py | Replaces hardcoded hostname allowlist with is_github_hostname() utility |
| tests/unit/test_github_host.py | New test file covering default_host(), is_github_hostname(), and sanitize_token_url_in_message() |
| tests/test_apm_package_models.py | Updates tests to use github_host utilities and adds GHE URL parsing test cases |
| tests/test_github_downloader_token_precedence.py | Updates URL assertions to use github_host utilities for consistency |
| tests/unit/test_registry_integration.py | Updates mock URLs to use default_host() for consistency |
| tests/unit/test_registry_client.py | Updates mock URLs to use default_host() for consistency |
| CONTRIBUTING.md | Updates testing instructions to recommend uv for running tests, adds fallback instructions for standard venv |
| class DependencyReference: | ||
| """Represents a reference to an APM dependency.""" | ||
| repo_url: str # e.g., "user/repo" or "github.com/user/repo" | ||
| host: Optional[str] = None # Optional host (github.com or enterprise host) |
There was a problem hiding this comment.
The comment describes the host field but doesn't explain when it's None vs when it's populated. Consider clarifying that host is extracted from the parsed dependency string and defaults to default_host() if not explicitly specified.
| host: Optional[str] = None # Optional host (github.com or enterprise host) | |
| host: Optional[str] = None # Host (e.g., github.com or enterprise host); extracted from the dependency string if specified, otherwise None. If None, defaults to default_host() when needed. |
| # Ensure host is set for enterprise repos | ||
| if getattr(dep_ref, 'host', None): | ||
| self.github_host = dep_ref.host |
There was a problem hiding this comment.
This pattern of checking and setting self.github_host is duplicated three times (lines 199-200, 213-214, 228-229). Consider extracting this into a helper method or setting it once before the conditional blocks to reduce duplication.
The runtime manager was calling setup_runtime_environment() without passing the env dict, which caused it to create a new os.environ.copy() and lose any tokens set in the CI environment. This fixes integration test failures where GITHUB_TOKEN and other secrets configured in GitHub Actions weren't reaching the runtime setup scripts, causing unauthenticated API requests and rate limiting. Fixes integration test failures in PR #8 and similar CI environments.
Co-authored-by: Copilot <[email protected]>
|
We will need to document |
…osoft#8) - Drop unittest.TestCase from all 6 test classes - Replace self.assert* with bare assert statements - Replace tempfile.TemporaryDirectory with tmp_path fixture - Remove unused imports (tempfile, unittest)
- Narrow except Exception to except ImportError for lazy marketplace import (comment #1) - Fix provenance key mismatch: use dep identity instead of canonical for lockfile lookup (comment #2) - Include subdir in git-subdir source resolution with path traversal validation (comment #3) - Include relative path in relative source resolution with traversal validation (comment #4) - Sanitize marketplace name in cache file paths to prevent path traversal (comment #5) - Fix docs: stale-if-error, not stale-while-revalidate (comment #6) - Consolidate CHANGELOG entries into single line with (#503) (comment #7) - Remove unused _SUPPORTED_SOURCE_TYPES set (comment #8) - Let auth errors propagate in _auto_detect_path instead of swallowing (comment #9) - Validate marketplace --name against [a-zA-Z0-9._-]+ charset (comment #10) - Fix doc examples to use identifier-compatible names (comments #11, #12) - Update tests to match corrected resolver behavior, add traversal tests Co-authored-by: Copilot <[email protected]>
…covery + governance (#503) * Initial plan * Initial plan for marketplace integration Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <[email protected]> * feat: marketplace integration core implementation - Add marketplace/ package: models, errors, registry, client, resolver - Add marketplace CLI commands: add, list, browse, update, remove, search - Add lockfile provenance fields: discovered_via, marketplace_plugin_name - Add install hook for NAME@MARKETPLACE syntax pre-parse intercept - Wire marketplace commands in cli.py Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <[email protected]> * docs: add marketplace integration guide and CLI reference - Create guides/marketplaces.md covering marketplace concepts, registration, browsing, search, install syntax, provenance tracking, and cache behavior - Add apm marketplace and apm search command sections to cli-commands.md - Update apm install arguments to include NAME@MARKETPLACE syntax - Update plugins.md Finding Plugins section with marketplace cross-refs Co-authored-by: danielmeppiel <[email protected]> * docs: fix marketplace.json format and lockfile field names to match implementation - Use array-based plugins format matching models.py parser expectations - Use discovered_via and marketplace_plugin_name matching lockfile.py fields - Document both Copilot CLI (repository/ref) and Claude Code (source) formats Co-authored-by: danielmeppiel <[email protected]> * docs: fix git-subdir and relative source descriptions to match resolver - git-subdir uses separate repo and subdir fields - Relative string sources resolve to marketplace repo subdirectory Co-authored-by: danielmeppiel <[email protected]> * feat: add marketplace unit tests and docs - 114 unit tests across 8 test files covering all marketplace modules - New marketplace guide at docs/src/content/docs/guides/marketplaces.md - Updated CLI reference with marketplace and search commands - Updated plugins guide with marketplace integration section - CHANGELOG entry for marketplace feature Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <[email protected]> * refactor: address code review feedback - Use List[MarketplacePlugin] from typing instead of lowercase generic - Eliminate duplicated condition in install.py marketplace intercept - Restructure control flow for clarity Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <[email protected]> * fix: address all 12 PR review comments on marketplace integration - Narrow except Exception to except ImportError for lazy marketplace import (comment #1) - Fix provenance key mismatch: use dep identity instead of canonical for lockfile lookup (comment #2) - Include subdir in git-subdir source resolution with path traversal validation (comment #3) - Include relative path in relative source resolution with traversal validation (comment #4) - Sanitize marketplace name in cache file paths to prevent path traversal (comment #5) - Fix docs: stale-if-error, not stale-while-revalidate (comment #6) - Consolidate CHANGELOG entries into single line with (#503) (comment #7) - Remove unused _SUPPORTED_SOURCE_TYPES set (comment #8) - Let auth errors propagate in _auto_detect_path instead of swallowing (comment #9) - Validate marketplace --name against [a-zA-Z0-9._-]+ charset (comment #10) - Fix doc examples to use identifier-compatible names (comments #11, #12) - Update tests to match corrected resolver behavior, add traversal tests Co-authored-by: Copilot <[email protected]> * fix: Copilot CLI format compatibility and marketplace provenance bugs Bug #1 - Format incompatibility with awesome-copilot marketplace: - Parser now accepts 'source' key (Copilot CLI) as type discriminator fallback when 'type' key is absent, normalizing to 'type' for resolvers - GitHub source resolver now accepts 'path' field (Copilot CLI) as virtual subdirectory, same as 'subdir' in git-subdir sources - Path traversal validation applied to 'path' field - Fixes: 8 of 62 plugins in awesome-copilot that use github source objects with 'source'+'path' keys instead of 'type'+'subdir' Bug #2 - Lockfile provenance never written: - Root cause: install passed raw marketplace refs (NAME@MARKETPLACE) as only_packages, but DependencyReference.parse() can't parse those, so identity filtering removed all deps -> 'already installed' - Fix: use validated_packages (canonical owner/repo strings) instead of raw click argument for only_pkgs Both bugs verified fixed via E2E tests against real marketplaces: - github/awesome-copilot (62 plugins) - anthropics/skills (3 plugins) - microsoft/azure-skills (1 plugin) Co-authored-by: Copilot <[email protected]> * feat: scope marketplace search to QUERY@MARKETPLACE format Search now requires QUERY@MARKETPLACE (e.g. apm search security@skills) to eliminate name collisions across marketplaces. Added search_marketplace() client function for single-marketplace search. - Rejects bare queries without @ — clear error with usage example - Validates marketplace exists before searching - Updated docs/guides/marketplaces.md with new syntax - 7 test cases: format validation, unknown marketplace, results, no results Co-authored-by: Copilot <[email protected]> * docs: update CLI reference and plugins guide for scoped search syntax Align all documentation with QUERY@MARKETPLACE search format. Co-authored-by: Copilot <[email protected]> * refactor: use centralized path_security for marketplace traversal checks Replace 3 ad-hoc '..' in x.split('/') checks in marketplace/resolver.py with validate_path_segments() from utils/path_security.py. Add defense-in-depth validate_path_segments() call to _sanitize_cache_name() in client.py. This ensures marketplace code uses the same cross-platform path safety utilities (backslash normalization, single-dot rejection) as the rest of APM. Co-authored-by: Copilot <[email protected]> * docs: add path safety rule to copilot-instructions.md Directs contributors to use validate_path_segments() and ensure_path_within() from utils/path_security.py instead of ad-hoc traversal checks. Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: danielmeppiel <[email protected]> Co-authored-by: danielmeppiel <[email protected]> Co-authored-by: Copilot <[email protected]>
Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps pyproject.toml + uv.lock to 0.11.0. Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because this release ships one BREAKING removal (`apm marketplace build` -> exits 2, use `apm pack`) plus several net-new features (Dev Container Feature, Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack` unification, multi-org `apps[]`). Strict semver in 0.x: minor for features-with-break, patch only for bugfixes. Milestone admin (done out-of-band): - Renamed milestone #8 `0.10.1` -> `0.11.0` - Created milestone #9 `0.12.0` as next-up bucket - Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0` - 6 closed items stay in `0.11.0` PRs shipping in 0.11.0 (22 commits since v0.10.0): User-facing features: - #1042/#722 `apm pack` unifies bundle + marketplace.json (BREAKING: `apm marketplace build` removed) - #1038 `marketplace:` block in apm.yml + `apm marketplace migrate` - #803 /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives - #861 Dev Container Feature `ghcr.io/microsoft/apm/apm-cli` - #982/#984 shared/apm.md `apps:` array for cross-org private packages - #820 `target:` in apm.yml validates at parse time - #1032 `apm marketplace add` honors manifest.name (Claude Code parity) - #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms User-facing fixes: - #1015 ADO Entra ID auth + `apm install --update` pre-flight abort - #1019/#1020 GEMINI.md only created when target requested - #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms - #1018 POSIX paths in auto-discovery output (Windows compat) - #996 drop stray 'specify' from generated file footer Maintainer tooling: - #1043 NOTICE.md per CELA template - #1045/#1044 NOTICE drift gate + license-policy gate in CI - #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut) - #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1 - #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file - #1022 review-panel: true fan-out + binary verdict + label automation - #918 complexity audit + benchmarks suite - #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder) Files changed: - pyproject.toml: 0.10.0 -> 0.11.0 - uv.lock: regenerated (version field only) - CHANGELOG.md: [Unreleased] promoted to [0.11.0] - 2026-04-29 NOTICE drift check passes against the bumped lockfile. Co-authored-by: Copilot <[email protected]>
Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps pyproject.toml + uv.lock to 0.11.0. Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because this release ships one BREAKING removal (`apm marketplace build` -> exits 2, use `apm pack`) plus several net-new features (Dev Container Feature, Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack` unification, multi-org `apps[]`). Strict semver in 0.x: minor for features-with-break, patch only for bugfixes. Milestone admin (done out-of-band): - Renamed milestone #8 `0.10.1` -> `0.11.0` - Created milestone #9 `0.12.0` as next-up bucket - Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0` - 6 closed items stay in `0.11.0` PRs shipping in 0.11.0 (22 commits since v0.10.0): User-facing features: - #1042/#722 `apm pack` unifies bundle + marketplace.json (BREAKING: `apm marketplace build` removed) - #1038 `marketplace:` block in apm.yml + `apm marketplace migrate` - #803 /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives - #861 Dev Container Feature `ghcr.io/microsoft/apm/apm-cli` - #982/#984 shared/apm.md `apps:` array for cross-org private packages - #820 `target:` in apm.yml validates at parse time - #1032 `apm marketplace add` honors manifest.name (Claude Code parity) - #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms User-facing fixes: - #1015 ADO Entra ID auth + `apm install --update` pre-flight abort - #1019/#1020 GEMINI.md only created when target requested - #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms - #1018 POSIX paths in auto-discovery output (Windows compat) - #996 drop stray 'specify' from generated file footer Maintainer tooling: - #1043 NOTICE.md per CELA template - #1045/#1044 NOTICE drift gate + license-policy gate in CI - #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut) - #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1 - #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file - #1022 review-panel: true fan-out + binary verdict + label automation - #918 complexity audit + benchmarks suite - #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder) Files changed: - pyproject.toml: 0.10.0 -> 0.11.0 - uv.lock: regenerated (version field only) - CHANGELOG.md: [Unreleased] promoted to [0.11.0] - 2026-04-29 NOTICE drift check passes against the bumped lockfile. Co-authored-by: Copilot <[email protected]>
* chore(release): cut 0.11.0 Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps pyproject.toml + uv.lock to 0.11.0. Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because this release ships one BREAKING removal (`apm marketplace build` -> exits 2, use `apm pack`) plus several net-new features (Dev Container Feature, Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack` unification, multi-org `apps[]`). Strict semver in 0.x: minor for features-with-break, patch only for bugfixes. Milestone admin (done out-of-band): - Renamed milestone #8 `0.10.1` -> `0.11.0` - Created milestone #9 `0.12.0` as next-up bucket - Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0` - 6 closed items stay in `0.11.0` PRs shipping in 0.11.0 (22 commits since v0.10.0): User-facing features: - #1042/#722 `apm pack` unifies bundle + marketplace.json (BREAKING: `apm marketplace build` removed) - #1038 `marketplace:` block in apm.yml + `apm marketplace migrate` - #803 /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives - #861 Dev Container Feature `ghcr.io/microsoft/apm/apm-cli` - #982/#984 shared/apm.md `apps:` array for cross-org private packages - #820 `target:` in apm.yml validates at parse time - #1032 `apm marketplace add` honors manifest.name (Claude Code parity) - #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms User-facing fixes: - #1015 ADO Entra ID auth + `apm install --update` pre-flight abort - #1019/#1020 GEMINI.md only created when target requested - #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms - #1018 POSIX paths in auto-discovery output (Windows compat) - #996 drop stray 'specify' from generated file footer Maintainer tooling: - #1043 NOTICE.md per CELA template - #1045/#1044 NOTICE drift gate + license-policy gate in CI - #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut) - #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1 - #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file - #1022 review-panel: true fan-out + binary verdict + label automation - #918 complexity audit + benchmarks suite - #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder) Files changed: - pyproject.toml: 0.10.0 -> 0.11.0 - uv.lock: regenerated (version field only) - CHANGELOG.md: [Unreleased] promoted to [0.11.0] - 2026-04-29 NOTICE drift check passes against the bumped lockfile. Co-authored-by: Copilot <[email protected]> * chore(changelog): tighten 0.11.0 entries to lead with user impact Co-authored-by: Copilot <[email protected]> * chore(changelog): move Dev Container Feature to Maintainer tooling (not yet published) Co-authored-by: Copilot <[email protected]> * chore(changelog): de-dupe within 0.11.0 (combine #722 Removed bullets, drop #820 Fixed pointer) Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
…#1073) * docs(notice): rename NOTICE.md -> NOTICE; add CLA third-party section Two changes, one file rename: 1. Rename NOTICE.md -> NOTICE, matching the Apache / CNCF convention used by upstream third-party-attribution files (kubernetes-sigs/kro, kubernetes-sigs/headlamp, etc.). The .md extension was non-idiomatic for a generated legal artifact -- NOTICE files are read by tooling (license scanners, SBOM generators) that match on the bare filename. Generator (scripts/generate-notice.py), Makefile target, and the NOTICE Drift Check workflow are all updated to operate on the extension-less path. 2. Add a 'Submitted on behalf of a third-party' section to NOTICE, crediting five contributors whose pull requests landed before the microsoft-github-policy-service CLA bot recorded a signature on file. The repo transferred from danielmeppiel/awd-cli to the microsoft org; some early PRs predate CLA enforcement, and we could not retroactively reach all contributors. Mirrors section 7 of common CLA texts (the wording adopted by CNCF NOTICE files). Driven by a new _third_party_submissions block in scripts/notice-metadata.yaml -- legally-significant wording stays alongside the per-component data, not buried in code. Contributors named (verified via Check Runs API against the microsoft-github-policy-service app, license/cla check on every merged PR by each suspected author): - @pofallon (PR #4) - @richgo (PRs #8, #25, #26, #33, #34) - @ryanfk (PR #92 -- bot ran with conclusion=null, output: 'Contributor License Agreement is not agreed yet.') - @foutoucour (PR #108) - @Jah-yee (PR #184) Listed contributors who later sign the CLA (or who were signed under a different GitHub account at the time) can request removal via issue. Co-authored-by: Copilot <[email protected]> * docs(notice): trim third-party section preamble Strip the historical/CNCF-citation paragraph and the verbatim CLA-section-7 quote. Keep only the active sentence (what the listing means + how to request removal). Co-authored-by: Copilot <[email protected]> * docs(notice): address PR #1073 review Three fixes from copilot-pull-request-reviewer: 1. Drop spurious leading '---' separator in the third-party-submissions renderer. render_component already ends each component with '---\n\n', so prepending another '---' produced two consecutive separators in NOTICE. Verified: separator count dropped from 17 to 16. 2. Sweep stale 'NOTICE.md' references in scripts/generate-notice.py (top-level docstring, Modes section, ComponentMeta and DepSpec field docstrings). The constant was renamed; the docs lagged. 3. Append (#1073) PR refs to both CHANGELOG entries; ASCII-correct the arrow ('->' instead of '->'). Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Copilot <[email protected]>
…e-commands Address the 5 required findings from the APM Review Panel on PR microsoft#1046 (round 2) by replacing the hardcoded cursor branch and the warn()-only notification with a unified consent gate. Changes: - targets.py: add TargetProfile.requires_executable_consent (default False); set True on the cursor entry only. Documents the asymmetric- consent risk for claude/opencode/gemini in the docstring (panel finding microsoft#5: deferred to avoid breaking existing installs). - command_integrator.py: introduce should_deploy_executable_commands() helper. integrate_commands_for_target now accepts allow_executable_commands and skips deployment with a warn-level diagnostic naming the exact CLI flag when consent is required but absent. Replaces the cursor-branded warn() block (Threat microsoft#8: warn is notification, not consent). - install.py: add --allow-executable-commands click option and field on the local InstallContext; thread to _install_apm_dependencies and through InstallRequest -> InstallService -> run_install_pipeline. - services.py / pipeline.py / context.py / template.py / phases: plumb allow_executable_commands through the install pipeline. The dispatch loop only forwards the kwarg to the commands integrator. Tests: - New TestExecutableConsentGate class covering: helper short-circuit for non-consent targets, helper block/allow on cursor, gated-skip result + warning when flag absent, deploy when flag present, and parametrized regression that claude/opencode/gemini are unaffected. - Existing cursor deployment tests updated to pass allow_executable_commands=True. - test_install_context.py: include allow_executable_commands in the expected fields tuple. Docs: - cli-commands.md and packages/apm-guide commands.md: document the new flag and the cursor opt-in. - CHANGELOG.md: reframe the cursor entry around the user-visible opt-in flag and Threat microsoft#8 rationale. Closes panel findings 1, 3, 4 from PR microsoft#1046 round 2. Finding 5 (asymmetric consent across slash-command targets) is documented in the TargetProfile docstring as deferred -- flipping the flag for claude/opencode/gemini would silently break existing installs. Co-authored-by: Copilot <[email protected]>
…efault, --allow-cursor-commands Round 4 panel raised 8 required findings against the cursor-commands gate. Round 5 addresses all of them: Architecture (python-architect) - Add frozen IntegrateOptions dataclass with extra_kwargs(prim) helper in install/services.py. Dispatch loop is now branchless — no more inline 'if _prim_name == "commands"' special case. - Hoist pkg_name once at top of integrate_commands_for_target. - Move executable-commands gate before find_prompt_files in the happy path; keep one find call inside the gate-skipped branch so the user-facing skip warning includes an exact count. CLI logging (cli-logging-expert) - _post_install_summary surfaces the consent flag in --verbose output: ' allow-cursor-commands: true|false' (gated on logger.verbose). - Add _should_emit_passthrough_notice helper + _passthrough_notified instance set on CommandIntegrator. One-shot info diagnostic fires once per (install, target) on Cursor installs explaining that Cursor command files keep the Claude-compatible frontmatter subset intentionally for cross-tool compatibility. - Combine missing-.cursor/-dir hint with consent-flag mention so users see both signals in one diagnostic. DevX UX (devx-ux-expert) - Rename CLI flag --allow-executable-commands -> --allow-cursor-commands (target-scoped, user-friendly). Internal pipeline parameter remains allow_executable_commands so future targets can share the gate plumbing via their own --allow-<target>-commands flags. Mapping happens at the CLI boundary in install(). - Update skip warning to name the new flag. Supply-chain security (supply-chain-security-expert) - Flip TargetProfile.requires_executable_consent default to True at the dataclass level. New targets now refuse command deployment unless an opt-out is explicit. Add explicit =False with rationale comment on each existing target (claude, opencode, gemini), each referencing enterprise/security.md. - Add new '## Post-install code execution (Threat microsoft#8)' section to docs/src/content/docs/enterprise/security.md with the per-target posture matrix (cursor=gated, claude/opencode/gemini=opt-out), rationale for the asymmetry, npm --ignore-scripts mental model, and future-targets guidance. - Remove dead bool() cast in should_deploy_executable_commands. - Remove inline TODO comments referencing PR URL. OSS growth (oss-growth-hacker) - Rewrite CHANGELOG entry to lead with user outcome ('Cursor users can install APM package slash commands with one flag…'). Security rationale moved to end. Cursor 1.6+ lifecycle note included inline. - Add Cursor 1.6+ lifecycle caveat to: - docs/.../integrations/ide-tool-integration.md - docs/.../reference/cli-commands.md - packages/apm-guide/.apm/skills/apm-usage/commands.md - packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Tests - Update test assertions to use new flag name. - Add requires_executable_consent=False to the synthetic test profile in tests/unit/integration/test_data_driven_dispatch.py (consequence of fail-closed default). - Full unit suite: 6924 passed. Round 5 panel: 6/6 specialists APPROVE; CEO synthesizer APPROVE. Co-authored-by: Copilot <[email protected]>
…fault Cursor slash commands have identical invocation semantics to Claude/OpenCode/Gemini -- the user must type /cmdname to invoke. They do not auto-execute on disk-write or IDE startup, so the npm --ignore-scripts / Threat microsoft#8 framing was incorrect. The gate created artificial asymmetry between targets with identical UX and violated portable-by-manifest. Removed: - --allow-cursor-commands CLI flag - TargetProfile.requires_executable_consent field - should_deploy_executable_commands() helper + gate block - IntegrateOptions per-primitive routing (only consumer was the gate) - allow_executable_commands plumbing through 7 layers - Threat microsoft#8 framing from docs + CHANGELOG Kept: - Cursor commands PrimitiveMapping (the actual feature) - Lossy-frontmatter diagnostics.warn() per file (real value: surfaces dropped Cursor-specific keys to package authors) - Cursor 1.6+ lifecycle note (Cursor de-emphasizing commands) Co-authored-by: Copilot <[email protected]>
* feat(cursor): add slash command support for Cursor 1.6+ Register the "commands" primitive in the Cursor target profile so apm install deploys .prompt.md files to .cursor/commands/*.md when a .cursor/ directory is present. Cursor 1.6 introduced custom slash commands [1] stored as plain Markdown in .cursor/commands/. Cursor has since de-emphasized commands in favor of skills/rules [2], but as long as it supports the commands surface it makes sense to deploy to it. Cursor itself does not consume YAML frontmatter, but we deliberately emit Claude-compatible frontmatter (description, allowed-tools, arguments, argument-hint) rather than stripping it. The frontmatter is harmless to Cursor -- the file is just a context-to-LLM routing mechanism -- and in practice, leaving structured input hints in the file makes the commands noticeably more successful at inferring required inputs from the user. This reuses the existing integrate_command() path (the else branch in CommandIntegrator.integrate_commands_for_target), so the only production code change is three lines in targets.py. Testing strategy: - Unit tests: opt-in guard, single/multi deploy, frontmatter preservation, sync removal, missing-dir graceful no-op - Integration tests: target gating dispatch (mocked integrators) and full end-to-end through integrate_package_primitives with real integrators verifying file output [1] https://cursor.com/changelog/1-6 [2] https://cursor.com/docs/plugins Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(cursor): address review feedback on command integration - Docs: change "frontmatter preserved" to "normalized to supported command frontmatter" -- the shared command transformer keeps only description, allowed-tools, model, and argument-hint, dropping unknown keys like author or custom fields - Tests: rename test_frontmatter_preserved to test_frontmatter_normalized_to_supported_subset and assert that non-whitelisted fields (author, custom-field) are dropped - Tests: fix test_sync_removes_apm_commands to use manifest-based sync (managed_files set) instead of the legacy *-apm.md glob fallback, matching how Cursor commands are actually deployed Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: ruff import organization in cursor command tests Co-authored-by: Copilot <[email protected]> * test/integration: address Copilot review on Cursor commands (#1046) - targets.py: switch Cursor commands format_id from cursor_command to claude_command and document why. The shared command transformer is what actually runs today; the previous tag mis-advertised a Cursor- specific writer that does not exist and would silently lie about preserving Cursor-only frontmatter (author/input/mcp/parameters). Switch back to a dedicated cursor_command tag only when an integrator branch is added that preserves full Cursor metadata verbatim. - test_command_integrator.py: drop redundant per-test reimports of shutil/tempfile (PEP 8 nit, both are imported at module top). Other Copilot comments were already covered: - docs already say 'normalized to supported command frontmatter' - test_sync_removes_managed_commands exercises Cursor manifest mode - test_frontmatter_normalized_to_supported_subset asserts non-whitelisted fields (author, custom-field) are dropped. Co-authored-by: Copilot <[email protected]> * style: ruff format test_command_integrator.py Co-authored-by: Copilot <[email protected]> * fix(cursor-commands): warn on dropped frontmatter, traversal guards, target-aware diagnostics Address all 10 required findings from APM Review Panel on PR #1046: - Extract _make_package() to module-level test helper (DRY across 6 classes) - Make integrate_command() target-aware via target_name kwarg; emit 'command arguments' instead of Claude-branded message for Cursor installs - Detect frontmatter keys not preserved by the shared claude_command transformer (author, mcp, parameters, ...) and surface diagnostics.warn() per file so users see the lossy transform at install time - Add install-time info note when .cursor/ is missing instead of silently skipping deployment (improves CI-vs-dev discoverability) - Validate base_name with validate_path_segments() and ensure_path_within() before joining into commands_dir to close path-traversal vector via malicious package filenames - Harden find_files_by_glob() to reject hardlinks/files whose resolved path escapes package_path (symlink check did not catch hardlinks) - Surface a one-time IDE consent warning per package when Cursor commands are deployed since they become invokable as IDE slash commands - Add TODO(cursor-command-format) with tracking issue URL on both targets.py and integrate_command() to make the shared-transformer debt trackable - Add commands_cursor identity entry to _BUCKET_ALIASES for explicit documentation parity with commands_opencode - CHANGELOG entry under [Unreleased] and ide-tool-integration.md note about the frontmatter subset limitation - New TestCursorCommandPanelFindings regression class covers all five panel-driven behaviours (dropped-key warn, traversal rejection, target-aware diagnostics, IDE consent, skip note) Verified: 6939 unit tests pass; ruff format + ruff check clean. Co-authored-by: Copilot <[email protected]> * fix(install): gate cursor command deployment behind --allow-executable-commands Address the 5 required findings from the APM Review Panel on PR #1046 (round 2) by replacing the hardcoded cursor branch and the warn()-only notification with a unified consent gate. Changes: - targets.py: add TargetProfile.requires_executable_consent (default False); set True on the cursor entry only. Documents the asymmetric- consent risk for claude/opencode/gemini in the docstring (panel finding #5: deferred to avoid breaking existing installs). - command_integrator.py: introduce should_deploy_executable_commands() helper. integrate_commands_for_target now accepts allow_executable_commands and skips deployment with a warn-level diagnostic naming the exact CLI flag when consent is required but absent. Replaces the cursor-branded warn() block (Threat #8: warn is notification, not consent). - install.py: add --allow-executable-commands click option and field on the local InstallContext; thread to _install_apm_dependencies and through InstallRequest -> InstallService -> run_install_pipeline. - services.py / pipeline.py / context.py / template.py / phases: plumb allow_executable_commands through the install pipeline. The dispatch loop only forwards the kwarg to the commands integrator. Tests: - New TestExecutableConsentGate class covering: helper short-circuit for non-consent targets, helper block/allow on cursor, gated-skip result + warning when flag absent, deploy when flag present, and parametrized regression that claude/opencode/gemini are unaffected. - Existing cursor deployment tests updated to pass allow_executable_commands=True. - test_install_context.py: include allow_executable_commands in the expected fields tuple. Docs: - cli-commands.md and packages/apm-guide commands.md: document the new flag and the cursor opt-in. - CHANGELOG.md: reframe the cursor entry around the user-visible opt-in flag and Threat #8 rationale. Closes panel findings 1, 3, 4 from PR #1046 round 2. Finding 5 (asymmetric consent across slash-command targets) is documented in the TargetProfile docstring as deferred -- flipping the flag for claude/opencode/gemini would silently break existing installs. Co-authored-by: Copilot <[email protected]> * fix(install): address round 4 panel — IntegrateOptions, fail-closed default, --allow-cursor-commands Round 4 panel raised 8 required findings against the cursor-commands gate. Round 5 addresses all of them: Architecture (python-architect) - Add frozen IntegrateOptions dataclass with extra_kwargs(prim) helper in install/services.py. Dispatch loop is now branchless — no more inline 'if _prim_name == "commands"' special case. - Hoist pkg_name once at top of integrate_commands_for_target. - Move executable-commands gate before find_prompt_files in the happy path; keep one find call inside the gate-skipped branch so the user-facing skip warning includes an exact count. CLI logging (cli-logging-expert) - _post_install_summary surfaces the consent flag in --verbose output: ' allow-cursor-commands: true|false' (gated on logger.verbose). - Add _should_emit_passthrough_notice helper + _passthrough_notified instance set on CommandIntegrator. One-shot info diagnostic fires once per (install, target) on Cursor installs explaining that Cursor command files keep the Claude-compatible frontmatter subset intentionally for cross-tool compatibility. - Combine missing-.cursor/-dir hint with consent-flag mention so users see both signals in one diagnostic. DevX UX (devx-ux-expert) - Rename CLI flag --allow-executable-commands -> --allow-cursor-commands (target-scoped, user-friendly). Internal pipeline parameter remains allow_executable_commands so future targets can share the gate plumbing via their own --allow-<target>-commands flags. Mapping happens at the CLI boundary in install(). - Update skip warning to name the new flag. Supply-chain security (supply-chain-security-expert) - Flip TargetProfile.requires_executable_consent default to True at the dataclass level. New targets now refuse command deployment unless an opt-out is explicit. Add explicit =False with rationale comment on each existing target (claude, opencode, gemini), each referencing enterprise/security.md. - Add new '## Post-install code execution (Threat #8)' section to docs/src/content/docs/enterprise/security.md with the per-target posture matrix (cursor=gated, claude/opencode/gemini=opt-out), rationale for the asymmetry, npm --ignore-scripts mental model, and future-targets guidance. - Remove dead bool() cast in should_deploy_executable_commands. - Remove inline TODO comments referencing PR URL. OSS growth (oss-growth-hacker) - Rewrite CHANGELOG entry to lead with user outcome ('Cursor users can install APM package slash commands with one flag…'). Security rationale moved to end. Cursor 1.6+ lifecycle note included inline. - Add Cursor 1.6+ lifecycle caveat to: - docs/.../integrations/ide-tool-integration.md - docs/.../reference/cli-commands.md - packages/apm-guide/.apm/skills/apm-usage/commands.md - packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Tests - Update test assertions to use new flag name. - Add requires_executable_consent=False to the synthetic test profile in tests/unit/integration/test_data_driven_dispatch.py (consequence of fail-closed default). - Full unit suite: 6924 passed. Round 5 panel: 6/6 specialists APPROVE; CEO synthesizer APPROVE. Co-authored-by: Copilot <[email protected]> * fix(cursor): drop --allow-cursor-commands gate, deploy commands by default Cursor slash commands have identical invocation semantics to Claude/OpenCode/Gemini -- the user must type /cmdname to invoke. They do not auto-execute on disk-write or IDE startup, so the npm --ignore-scripts / Threat #8 framing was incorrect. The gate created artificial asymmetry between targets with identical UX and violated portable-by-manifest. Removed: - --allow-cursor-commands CLI flag - TargetProfile.requires_executable_consent field - should_deploy_executable_commands() helper + gate block - IntegrateOptions per-primitive routing (only consumer was the gate) - allow_executable_commands plumbing through 7 layers - Threat #8 framing from docs + CHANGELOG Kept: - Cursor commands PrimitiveMapping (the actual feature) - Lossy-frontmatter diagnostics.warn() per file (real value: surfaces dropped Cursor-specific keys to package authors) - Cursor 1.6+ lifecycle note (Cursor de-emphasizing commands) Co-authored-by: Copilot <[email protected]> * fix(cursor-commands): act on PR #1046 panel review Apply the actionable findings from the apm-review-panel critique on PR #1046: * docs: fix `.opencode/command/*.md` -> `.opencode/commands/*.md` typo in the security guide (factual error in IDE integration map). * CHANGELOG: rewrite Cursor entry user-first; lifecycle caveat moves to a trailing sentence so the value prop reads first. * integrator: gate the one-shot 'cross-tool compatibility' notice on whether at least one file in the batch had dropped frontmatter keys. Previously fired on every Cursor install -> noise on the happy path. Cursor installs whose prompts only use the cross-tool subset now emit zero notice. Other targets (claude, opencode) unaffected. * integrator: rewrite the dropped-keys warn message to drop internal jargon ('shared command transformer'); use target-name framing ('not supported for cursor commands') and surface the canonical kebab-case key list (no camelCase aliases). * integrator: upgrade post-transform scan_text from WARN_POLICY to BLOCK_POLICY. When a critical finding is detected (e.g. a hidden tag char introduced by link resolution), skip the write and account for it in result.files_skipped. Pre-install BLOCK gate already scans source files, so this is defense-in-depth for the transform-introduced edge case; no regression for legitimate packages. * base_integrator: actually close the hardlink containment gap in find_files_by_glob. The prior is_relative_to() check did NOT catch hardlinks -- a hardlink resolves to its own path inside the package root. Add an st_nlink > 1 reject to prevent a malicious package shipping a hardlink to (e.g.) /etc/passwd from being read via integration. Update the inline comment to be accurate about what each guard actually does. Tests: * tests/unit/integration/test_base_integrator.py: new test_hardlink_escaping_package_root_is_excluded covering the st_nlink reject; cleaned up F821 noqa via real pytest import. * tests/unit/integration/test_command_integrator.py: 4 new tests in TestCursorCommandPanelFindings: - test_passthrough_notice_suppressed_on_clean_install - test_passthrough_notice_emitted_when_any_file_drops_keys - test_dropped_keys_warn_uses_user_facing_wording - test_critical_security_finding_blocks_write Updated the existing test_dropped_frontmatter_keys_warn assertion to match the new wording. * tests/integration/test_marketplace_plugin_integration.py: 2 new integration tests locking in cursor 1.6+ command deployment via the install pipeline (deploy when .cursor/ exists; respect opt-in by NOT creating .cursor/ when missing). Verification: uv run --extra dev ruff check src/ tests/ -> All checks passed! uv run --extra dev ruff format --check src/ tests/ -> 627 files OK uv run --extra dev pytest tests/unit ... -> 7222 passed Refs: #1046 (comment) Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Daniel Meppiel <[email protected]>
Add support for GitHub Enterprise (GHE) hostnames in dependency parsing so apm can correctly detect and normalize repository references hosted on self-managed GitHub instances.
What changed
Update the dependency parsing logic to accept and validate custom GHE hostnames in addition to github.com.
Preserve backwards compatibility for existing github.com parsing behavior.
Add unit tests covering:
standard github.com repo strings
GHE hostname variants (with and without ports)
edge cases and invalid hostnames
Why
Users running packages on self-hosted GitHub instances faced incorrect parsing of dependency references. This change allows apm to work in GHE environments.
How to test
Run the included unit tests: uv run pytest (test docs updated)
Manually verify parsing by providing dependency strings that reference a GHE hostname (e.g., acompany.ghe.com/owner/repo) and confirming correct repo extraction.
Notes
No behavior change for public GitHub users.
Backwards-compatible and covered by tests.