feat(install): support per-dependency agent subsets - #2657
Conversation
|
wind127 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
There was a problem hiding this comment.
Pull request overview
Adds per-dependency agent selection via manifest fields and repeatable --agent CLI options, with persistence through lockfiles, deployment, and audit replay.
Changes:
- Adds validated agent subsets and wildcard reset semantics.
- Threads selections through installation, locking, deployment, and policy checks.
- Updates schemas, specifications, documentation, conformance artifacts, and tests.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Review summary |
|---|---|
tests/unit/test_audit_policy_command.py |
Critical (3 votes): Update the expected baseline count from 9 to 10. |
tests/unit/policy/test_ci_checks.py |
Reviewed; no final comment. |
tests/unit/install/test_drift.py |
Reviewed; no final comment. |
tests/unit/install/test_agent_subset.py |
Reviewed; no final comment. |
tests/unit/deps/test_lockfile_field_properties.py |
Reviewed; no final comment. |
tests/unit/deps/test_lockfile_consumer_contract.py |
Reviewed; no final comment. |
tests/spec_conformance/test_manifest_reqs.py |
Reviewed; no final comment. |
tests/integration/test_architecture_intent_guards.py |
Reviewed; no final comment. |
src/apm_cli/policy/ci_checks.py |
Reviewed; no final comment. |
src/apm_cli/models/dependency/subsets.py |
Reviewed; no final comment. |
src/apm_cli/models/dependency/registry_entry.py |
Reviewed; no final comment. |
src/apm_cli/models/dependency/reference.py |
Reviewed; no final comment. |
src/apm_cli/models/dependency/object_fields.py |
Reviewed; no final comment. |
src/apm_cli/integration/agent_integrator.py |
Reviewed; no final comment. |
src/apm_cli/install/template.py |
Moderate (2 votes each): Scope CLI subsets by dependency, and use exact flat-name validation rather than leaf matching. |
src/apm_cli/install/services.py |
Reviewed; no final comment. |
src/apm_cli/install/service.py |
Reviewed; no final comment. |
src/apm_cli/install/request.py |
Reviewed; no final comment. |
src/apm_cli/install/pipeline.py |
Reviewed; no final comment. |
src/apm_cli/install/phases/lockfile.py |
Moderate (2 votes): Apply CLI subsets only to the selected dependency using dependency-keyed selection. |
src/apm_cli/install/package_resolution.py |
Reviewed; no final comment. |
src/apm_cli/install/entrypoint.py |
Reviewed; no final comment. |
src/apm_cli/install/drift.py |
Reviewed; no final comment. |
src/apm_cli/install/context.py |
Reviewed; no final comment. |
src/apm_cli/deps/lockfile.py |
Reviewed; no final comment. |
src/apm_cli/commands/install.py |
Reviewed; no final comment. |
src/apm_cli/commands/_apm_yml_writer.py |
Moderate (2 votes): Normalize and validate entries through parse_agent_subset, preserving None for clearing. |
packages/apm-guide/.apm/skills/apm-usage/governance.md |
Reviewed; no final comment. |
packages/apm-guide/.apm/skills/apm-usage/dependencies.md |
Reviewed; no final comment. |
packages/apm-guide/.apm/skills/apm-usage/commands.md |
Reviewed; no final comment. |
docs/src/content/docs/specs/openapm-v0.1.md |
Reviewed; no final comment. |
docs/src/content/docs/reference/manifest-schema.md |
Reviewed; no final comment. |
docs/src/content/docs/reference/lockfile-spec.md |
Reviewed; no final comment. |
docs/src/content/docs/reference/cli/install.md |
Reviewed; no final comment. |
docs/src/content/docs/reference/baseline-checks.md |
Reviewed; no final comment. |
docs/src/content/docs/integrations/ci-cd.md |
Reviewed; no final comment. |
docs/src/content/docs/enterprise/policy-reference.md |
Reviewed; no final comment. |
docs/src/content/docs/enterprise/enforce-in-ci.md |
Reviewed; no final comment. |
docs/src/content/docs/enterprise/drift-detection.md |
Reviewed; no final comment. |
docs/src/content/docs/concepts/the-three-promises.md |
Nit (3 votes): Correct the documented baseline check count and list to reflect ten checks. |
docs/src/content/docs/concepts/lifecycle.md |
Reviewed; no final comment. |
docs/public/specs/schemas/manifest-v0.1.schema.json |
Moderate (2 votes): Align the schema with parser constraints by rejecting whitespace-only and traversal-segment names. |
docs/public/specs/schemas/lockfile-v0.1.schema.json |
Reviewed; no final comment. |
docs/public/specs/manifests/openapm-v0.1.requirements.yml |
Reviewed; no final comment. |
CONFORMANCE.md |
Reviewed; no final comment. |
CONFORMANCE.json |
Reviewed; no final comment. |
CHANGELOG.md |
Reviewed; no final comment. |
Suppressed comments (10)
CHANGELOG.md:15
- The repository's changelog entries use a trailing PR reference such as
(#2435)and(closes #2482)inCHANGELOG.md:23-28, but this new entry ends only with(closes #2491). Append the current PR number while retaining the issue attribution so the entry follows the repository's per-PR format.
replay. `--agent '*'` resets the dependency to all agents. (closes #2491)
docs/public/specs/schemas/lockfile-v0.1.schema.json:57
- The lockfile contract describes
agent_subsetas flat agent names, but this schema accepts path-like values such asteam/planner. A schema-valid lockfile can therefore contain a selection that manifest parsing rejects and replay silently deploys nothing. Apply the same no-separator item pattern used by the manifestagentsfield.
"agent_subset": { "type": "array", "items": { "type": "string" } },
docs/src/content/docs/enterprise/policy-reference.md:411
- The new
agent-subset-consistencyrow is added here, but this baseline-check table still omits the existingdeployment-ledger-ownerscheck. The table is therefore incomplete and conflicts with the ten-check sequence documented elsewhere; add the ledger-owner row afterref-consistency.
| `agent-subset-consistency` | `agents:` selections in `apm.yml` match `agent_subset` in the lockfile |
src/apm_cli/commands/install.py:1038
- The help text points users to
apm.lock, the legacy filename, but this command persists the new selection inapm.lock.yaml(the filename used by the implementation and documentation). Use the current filename so users can find the persisted state.
help="Install only named agent(s) from a package. Repeatable and persisted in apm.yml and apm.lock. Additive across installs; use --agent '*' to reset to all agents.",
src/apm_cli/commands/install.py:407
- After this pin is set, the marketplace/direct-virtual branch serializes with
dependency_reference_to_yaml_entry, whose output only includes the Git/path/ref/alias fields. A named--agentpin is therefore omitted fromapm.ymlfor those dependencies, even though it reaches deployment and the lockfile; the next bare install deploys all agents and audit reports manifest/lock drift. Preservedep_ref.agent_subsetin that serializer.
apply_cli_agent_pin(
dep_ref,
agent_subset,
agent_subset_from_cli,
current_deps,
src/apm_cli/commands/install.py:1575
- When
apm install --agent NAMEis invoked without a positional package, the command skips_validate_and_add_packages_to_apm_yml, so no manifest entry is updated, but these context values still cause the lockfile builder to attach the CLI subset to dependency entries. This leavesapm.ymlandapm.lock.yamlinconsistent; the wildcard form is worse because it deploys all agents while leaving the old pin persisted. Reject--agentwithout package arguments or persist the selection for the affected manifest entries instead of applying an ephemeral override.
agent_subset=_agent_subset,
agent_subset_from_cli=bool(agent_names),
src/apm_cli/deps/lockfile.py:500
- Unlike manifest parsing, this new lockfile field is reconstructed with
list(...)and no type or flat-name validation. A malformed lockfile value such asagent_subset: plannerbecomes character names, whileteam/planneris accepted and replay then silently deploys no matching agent. Validate the non-empty value with the same agent-subset parser (preserving an empty value as the lockfile's "all" sentinel) before constructingLockedDependency.
agent_subset=list(data.get("agent_subset") or []),
src/apm_cli/install/package_resolution.py:398
normalize_and_merge_agent_subsetonly strips and deduplicates CLI values, so a value such asteam/planneris not rejected. The later component check compares leaf names, so this can pass whenplannerexists, then persist an invalidagentspath whileAgentIntegrator's exact flat-name match deploys nothing. Validate CLI values with the canonical flat-name parser before assigning them.
dep_ref.agent_subset = normalize_and_merge_agent_subset(
cli_subset,
current_deps,
identity,
dependency_reference_cls=dependency_reference_cls,
src/apm_cli/install/template.py:126
Materializationdeliberately setspackage_info=Nonewhen no targets exist (install/sources.py:471-477), but this new validation runs first and callsavailable_agent_names(m.package_info), which dereferences.install_path. A named--agenton a no-target install raisesAttributeErrorinstead of following the no-op path; move the existingm.package_info is None or not ctx.targetsguard before this validation.
src/apm_cli/models/dependency/reference.py:1919- When a selected agent is applied to a virtual shorthand such as
owner/repo/path, this structured-entry branch omitsself.virtual_path. Ordinary GitHub virtual shorthands do not take the GitLab/Artifactorydirect_virtual_resolvedpath, soapm.ymlis rewritten as the repository root and the next install loses the virtual-package boundary. Include the virtual path in this entry (or route it through the shared structured serializer).
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "prerelease": { "type": "boolean" }, | ||
| "path": { "type": "string" }, | ||
| "alias": { "type": "string" }, | ||
| "agents": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^[^/\\\\]+$" } }, |
There was a problem hiding this comment.
Fixed in 9893528. The manifest and lockfile schemas now reject blank-only names, ./.., and path separators, with conformance coverage for each case.
| the CI surface used by `apm audit --ci`. It runs 8 baseline | ||
| checks: lockfile-exists, ref-consistency, deployed-files-present, | ||
| no-orphans, skill-subset-consistency, config-consistency, | ||
| no-orphans, agent-subset-consistency, skill-subset-consistency, config-consistency, | ||
| content-integrity, and includes-consent. |
There was a problem hiding this comment.
Fixed in 9893528. The page now lists all ten checks, including deployment-ledger-owners, and the FAQ count is updated to ten.
| if field == "agents": | ||
| ref.agent_subset = sorted(set(subset)) if subset else None |
There was a problem hiding this comment.
Fixed in 9893528. Agent write-back now delegates to parse_agent_subset, so helper callers cannot persist invalid flat-name values.
| for locked_dep in lockfile.dependencies.values(): | ||
| merged = effective_deploy_agent_subset( | ||
| agent_subset_from_cli=self.ctx.agent_subset_from_cli, | ||
| cli_subset=self.ctx.agent_subset, | ||
| persisted_subset=locked_dep.agent_subset, |
There was a problem hiding this comment.
Fixed in 9893528. CLI agent overrides are keyed to explicitly selected direct dependencies; lockfile reconciliation touches only those keys. Added a regression test proving transitive and unselected entries retain their own subsets.
| if ctx.agent_subset_from_cli and ctx.agent_subset: | ||
| from apm_cli.install.outcome import require_requested_components | ||
| from apm_cli.integration.agent_integrator import AgentIntegrator | ||
|
|
||
| available_agents = AgentIntegrator.available_agent_names(m.package_info) |
There was a problem hiding this comment.
Fixed in 9893528. InstallContext now resolves --agent targets to direct dependency keys. Integration applies and validates the CLI subset only for the matching dep_key; transitive dependencies use their own persisted subset. Added an end-to-end direct-planner/transitive-writer regression test.
| available_agents = AgentIntegrator.available_agent_names(m.package_info) | ||
| if not require_requested_components( | ||
| diagnostics, | ||
| option="--agent", | ||
| component="agent", | ||
| requested=ctx.agent_subset, | ||
| available=available_agents, | ||
| package=dep_key, | ||
| ): |
There was a problem hiding this comment.
Fixed in 9893528. CLI values are normalized through parse_agent_subset, and agent availability uses exact matching (match_leaf=False), so team/planner is rejected instead of matching planner. Invalid CLI input leaves the manifest unchanged.
| # Only the ten baseline checks, including deployment ownership. | ||
| assert data["summary"]["total"] == 9 |
There was a problem hiding this comment.
Fixed in 9893528. The assertion now expects all ten baseline checks, and the audit policy test suite passes.
TL;DR
This adds per-dependency agent selection alongside the existing skill subset flow. Consumers can declare
agents:or repeatapm install --agent NAME; the normalized selection is persisted in the manifest and lockfile, enforced before cross-target deployment, and replayed by bare installs and audit.Note
Closes #2491, an accepted high-priority issue with no competing branch or PR when implementation started.
Problem (WHY)
skills:subset has no parallel agent primitive boundary, so selecting package content is inconsistent across primitive types.The concrete reproduction and requested semantics are documented in #2491: persist the subset in
apm.ymlandapm.lock.yaml, make bare installs deterministic, reject an empty list, and treat absence as “all agents.”Approach (WHAT)
agents:inclusion list to git, path, and registry dependency objects.--agent NAME; use--agent '*'to reset a dependency to all agents.agent_subsetvalue through request, resolution, lockfile, deployment, and audit replay boundaries.AgentIntegrator, before target-specific dialect expansion.req-mf-025, CI consistency checks, docs, and scenario tests.Implementation (HOW)
models/dependency/{subsets,object_fields,reference,registry_entry}.pyvalidates non-empty flat names and round-trips the new field across every dependency form.commands/{install,_apm_yml_writer}.pyexposes--agent, preserves additive pins, implements wildcard reset, and retains the historical command patch seam throughinstall/entrypoint.py.install/{request,service,pipeline,context,package_resolution,template,services}.pycarries and validates intent before handing the effective subset to primitive integration.deps/lockfile.py,install/phases/lockfile.py, andinstall/drift.pypersist sortedagent_subsetvalues and reconstruct them during audit replay.integration/agent_integrator.pyderives canonical flat names and filters source agents before translating them for each target.policy/ci_checks.pyaddsagent-subset-consistency, preventing manifest/lock drift in CI.CHANGELOG.mddescribe the same behavior.Legend: dashed nodes are the new intent and enforcement boundaries; existing target adapters remain unchanged.
flowchart LR subgraph Input[Dependency intent] C[--agent NAME] M[agents list] end subgraph Persist[Deterministic state] R[DependencyReference agent_subset] Y[apm.yml] L[apm.lock.yaml] end subgraph Deploy[Primitive deployment] V[Validate requested names] F[Filter source agents] T[Target adapters] end C --> R M --> R R --> Y R --> L Y --> V L --> V V --> F F --> T classDef new stroke-dasharray: 5 5; class C,M,R,L,V,F new;Trade-offs
--skill; an explicit wildcard is required to return to all agents.agents/agent_subsetmeans all agents; older manifests and lockfiles retain their behavior.Benefits
Validation
uv run pytest ...(focused model, install, lockfile, policy, and spec suites):uv run --extra dev ruff check src/ tests/andruff format --check:uv run --frozen --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/:Architecture, auth, and OpenAPM conformance guards:
Docs build: 123 pages generated; all 969 internal links valid.
Important
The full Windows unit run also contains pre-existing tests that require symlink privileges and a newer Git supporting
git init --initial-branch; those environment-limited cases could not run on this host. The focused scenarios and repository lint/architecture contracts above are green.Scenario Evidence
tests/unit/install/test_agent_subset.py::TestAgentSubsetInstall::test_cli_subset_is_additive_persisted_and_replayed--agent '*'tests/unit/install/test_agent_subset.py::TestAgentSubsetInstall::test_cli_subset_is_additive_persisted_and_replayedapm.ymltests/unit/install/test_agent_subset.py::TestAgentSubsetInstall::test_unknown_cli_agent_fails_without_persisting_pintests/unit/install/test_agent_subset.py::TestAgentSubsetModel::test_invalid_agent_subset_is_rejectedtests/unit/install/test_agent_subset.py::TestAgentSubsetModel::test_lockfile_round_trip_preserves_agent_subsettests/unit/install/test_drift.py::test_run_replay_threads_locked_primitive_subsetstests/unit/install/test_agent_subset.py::TestAgentSubsetModel::test_ci_baseline_detects_agent_subset_driftHow to test
planner.agent.md,reviewer.agent.md, andwriter.agent.md.apm install <package> --agent planner --target copilot; observe onlyplanner.agent.mdunder.github/agents/.--agent reviewer; observe both planner and reviewer, with sortedagentsandagent_subsetentries.apm install --target copilot; observe the same two agents.--agent '*'; observe all three agents and no persisted subset.PR checklist
Type of change
Testing
Spec conformance (OpenAPM v0.1)
docs/src/content/docs/specs/openapm-v0.1.mdupdated withreq-mf-025and Appendix C.docs/public/specs/manifests/openapm-v0.1.requirements.ymlupdated.@pytest.mark.req("req-mf-025")test added undertests/spec_conformance/.CONFORMANCE.{md,json}regenerated and committed.Closes #2491