fix(install): explain skipped symlinked agent sources (closes #2913) - #2932
fix(install): explain skipped symlinked agent sources (closes #2913)#2932Daniel Meppiel (danielmeppiel) wants to merge 3 commits into
Conversation
Retain source authorization and symlink rejection while reporting actionable agent-source warnings once per package source plan. Cover own-project and installed Git-package flows without expanding deployments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Symlinked sources may still be traversed, and symlink tests need portable Windows handling.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
src/apm_cli/install/deployable_source_plan.py — This warning claims symlinked directories are not traversed, but the agent path is still walked… |
What changed in this PR
Adds actionable warnings when symlinked agent sources are skipped during installation while preserving existing security filtering.
Changes:
- Threads diagnostics through source planning.
- Adds symlink rejection and target-gating tests.
- Updates authoring documentation.
| File | Summary |
|---|---|
tests/unit/install/test_agent_symlink_diagnostics.py |
Adds filesystem and CLI regression coverage. |
src/apm_cli/install/services.py |
Forwards diagnostic context into planning. |
src/apm_cli/install/deployable_source_plan.py |
Reports rejected symlinked sources. |
packages/apm-guide/.apm/skills/apm-usage/package-authoring.md |
Updates authoring guidance. |
docs/src/content/docs/producer/author-primitives/instructions-and-agents.md |
Documents symlink behavior and remediation. |
Suppressed comments (1)
tests/unit/install/test_agent_symlink_diagnostics.py:212
- This module is selected by the
windows_compatgate, but the new test cases create symlinks without handlingOSError/NotImplementedError. On a Windows runner without symlink privileges, the setup fails before the assertions; the repository's other symlink tests skip in this situation (for example,tests/unit/marketplace/test_marketplace_audit.py:427-431). Guard the symlink setup with a shared skip helper so the cross-platform gate remains portable.
@pytest.mark.windows_compat
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Prove req-sc-015 still excludes agent symlink files and directories from the authorized scan and materialization sets. Assert diagnostic output separately as APM behavior, without changing normative requirements. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 0 | Canonical owner retained; record legacy-discovery residual. |
| CLI Logging Expert | 0 | 0 | 0 | Accurate, actionable, ASCII-safe deferred warning. |
| DevX UX Expert | 0 | 0 | 0 | Clear remedy, once per source, unchanged exit behavior. |
| Supply Chain Security | 0 | 1 | 0 | No source/deployment expansion; no no-read claim. |
| OSS Growth Hacker | 0 | 0 | 0 | Changelog and consumer cross-link folded. |
| Doc Writer | 0 | 0 | 1 | Guidance accurate; suggested closer anchor is unnecessary. |
| Test Coverage | 0 | 0 | 0 | Exact-head consumer tests and callback mutation protect the change. |
| Performance Expert | 0 | 0 | 0 | No new walk, I/O or transport work. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 1 follow-up
- [Supply Chain Security / Python Architect] Legacy
AgentIntegrator.find_agent_filescan enumerate/read rejected sources before authorization filtering. This is explicitly recorded in the PR trade-offs and deferred: a separate hardening change should consume the canonical authorized source set and add a no-read regression.
Architecture
flowchart TD
A[apm install CLI] --> B[services.integrate_package_primitives]
B --> C[DeployableSourcePlan.create]
C --> D{_is_safe_source_path}
D -- contained real path --> E[add to plan.paths]
D -- has_symlink_component --> F[on_symlink=warn_agent_symlink]
F --> G[FS DiagnosticCollector.warn sanitized rel path]
E --> H[AgentIntegrator.integrate_agents_for_target]
H --> I[FS deploy authorized real files only]
G --> J[render_summary at command end]
I --> J
Recommendation
No remaining selected in-scope code changes. The known legacy-discovery residual is recorded in the PR body and below. Keep the existing consumer #agents link: the troubleshooting bullet already gives the complete remedy, and adding another heading would fragment the short Layout paragraph. The separate hardening follow-up must not introduce another symlink detector.
Full per-persona findings
Python Architect
- [recommended] Record pre-authorization discovery reads as an out-of-scope follow-up. The callback remains observability at the canonical guard.
CLI Logging Expert
No findings.
DevX UX Expert
No findings.
Supply Chain Security
- [recommended] The no-deployment warning does not mean no reads. Record the residual in the PR trade-offs and track future canonical authorized-only discovery separately.
OSS Growth Hacker
No findings.
Auth Expert -- inactive
No credentials, host classification or remote authorization behavior changed.
Doc Writer
- [nit] A dedicated remedy heading would provide a closer anchor. CEO does not select it: the existing section anchor is correct and the consumer bullet already contains the full remedy.
Test Coverage
No missing-coverage findings. Exact-head command: diagnostic module, paired conformance cases, and existing architecture mutation module selected with -k 'symlink or source_plan or source-plan': 26 passed, 339 deselected in 16.72s. These tests prove warning and deployment parity, not absence of legacy discovery reads.
Performance Expert
No findings.
This panel is advisory. It does not block merge.
Reservations carried from strategic-alignment
- User expressly approved diagnostic-only; retain source-plan symlink exclusion and exact deployed sets. Do NOT restore remote dereferencing or loosen has_symlink_component; earlier strategic proposal superseded. -- Addressed: callback observes unchanged rejection; final fold only narrows wording.
- Leave existing contained local-dependency preprocessing intact; req-sc-015 exclusion is normative, new warning itself is not a spec mandate. -- Addressed: local-copy behavior unchanged and tested; conformance assertions distinguish normative exclusion from diagnostic UX.
- No duplicate symlink detector; observe existing canonical guard with diagnostics once per source, avoiding per-target noise. -- Addressed: one plan callback shared across selected targets.
Folded in this run
- (Copilot + panel) Remove global no-traversal wording and pin accurate no-deployment sentence for both services routes --
aa95cf98543492cfe4d83d325659e456138409ff. - (Panel) Clarify callback's symlink-only observer contract, local-copy docs, consumer remedy cross-link and Unreleased changelog -- same commit.
- (Panel) Record residual legacy discovery reads -- current PR trade-offs.
Copilot signals reviewed
- Inline
3973081759(review5159932411): LEGIT wording overclaim, fixed inaa95cf985. Replied in-thread and resolved with the explicit scope distinction; no-read hardening is deferred. - Review
5159932411, suppressed Windows symlink skip suggestion: NOT-LEGIT for the supported current test contract; hosted Windows passed, and adding runtime privilege skips without a concrete failure would weaken the regression.
Deferred (out-of-scope follow-ups)
- Legacy
AgentIntegratorbefore-classification reads: the approved scope is actionable diagnostics with no change to deployment/discovery behavior. Route future discovery through the canonical authorized source set with a no-read regression in a separately reviewed hardening change. No follow-up issue was opened in this run.
Regression-trap evidence (mutation-break gate)
- Remove the reporting callback from the existing symlink rejection branch: 19 failed, 3 passed in 2.25s across all 22 new cases; callback restored.
- New narrowed-sentence assertion before the wording fold: 2 failed, 2 passed, 16 deselected in 2.21s; the real-directory controls passed.
- Original FIX-wave service-wiring and source-plan mutation evidence is retained in the PR body.
Lint contract
All seven current canonical lint gates plus architecture boundaries passed before push, including Ruff over src/, tests/ and architecture scripts, formatting, pylint R0801, YAML I/O, 2100-line and portable-path guards, and auth signals. Architecture boundaries passed again on exact head aa95cf985.
CI
All 18 checks concluded SUCCESS, NEUTRAL or SKIPPED on aa95cf985, with zero CI recovery iterations or retries on this head: CI, spec, aggregate.
Historical FIX-wave provenance retained: previous head 068766eef had one HTTP-thread semaphore timeout in run 34406073853 job 102649234979, one successful failed-job retry, and one stale aggregate refresh (34406073846). These are not current-head evidence.
Mergeability status
| PR | head SHA | CEO stance | iters | folds | defers | Copilot rounds | CI | mergeable | mergeStateStatus | notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #2932 | aa95cf985 |
ship_with_followups | 2 | 3 | 1 | 2 | green | MERGEABLE | BLOCKED | Repository policy remains unsatisfied; no approval or merge permission claimed. |
Convergence
Two outer iterations and two Copilot rounds. Selected in-scope folds are complete; one explicitly out-of-scope hardening follow-up remains. Terminal driver status is blocked by repository policy, not a claim of full merge-readiness. No auto-merge or fabricated approval.
Fold PR #2932 panel feedback without changing source authorization or deployment. Add release note and consumer guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Just wondering: Why don't you allow symlinks withing the same repository root? The way i see it this is binding the project shipping the agents to apm, whilst there are package manager tools as well. Looking forward to your vision. (Btw: we are taking the same approach for skills, which just works. So disallowing it for agents and not for skills might not be very consistent) |

fix(install): explain skipped symlinked agent sources
TL;DR
apm installnow reports skipped symlinked agent sources with the source path, the reason, and a real-file remedy. The warning is collected once during source planning, rather than repeated for each target. Source authorization, deployed files, and existing local-path dependency dereferencing are unchanged.Closes #2913.
Important
This is the maintainer-approved diagnostic-only fix, not a symlink-acceptance change. Source planning still excludes symlink files and does not traverse symlink directories. Legacy integrator discovery may still enumerate/read rejected candidates before authorization filtering; this PR does not claim to fix that separate behavior.
Problem (WHY)
.apm/agents -> ../agentsdeployed zero agents without explaining why. Real-filesystem regressions reproduced both missing warnings before the fix; both real-directory controls passed.Approach (WHAT)
has_symlink_componentrejection branch.DiagnosticCollectorand deferred warning renderer.Implementation (HOW)
src/apm_cli/install/deployable_source_plan.pysrc/apm_cli/install/services.pytests/unit/install/test_agent_symlink_diagnostics.pytests/spec_conformance/test_manifest_reqs.pyreq-sc-015; paired warning assertions are APM behavior, not a new spec mandate.CONFORMANCE.json,CONFORMANCE.mddocs/src/content/docs/producer/author-primitives/instructions-and-agents.mdpackages/apm-guide/.apm/skills/apm-usage/package-authoring.mddocs/src/content/docs/consumer/install-packages.mdCHANGELOG.mdArchitecture classification: owner-extension, observability only. The deterministic gate identifies Authorized deployable source paths at exact base
e38261c5db4d893d6ddebc3925742e4e3bd2ba74and headaa95cf98543492cfe4d83d325659e456138409ff.DeployableSourcePlanremains the sole authorization owner; integrators consume its unchanged path set. Existinginstall-deployment-source-planstatic guard and architecture baseline/mutation assertions pass. No authority is centralized, split or rerouted.Diagrams
The highlighted interaction is new; the rejected path stays outside the shared deployment plan.
sequenceDiagram participant C as Install command participant S as install.services participant P as DeployableSourcePlan participant D as DiagnosticCollector participant A as AgentIntegrator C->>S: integrate package primitives S->>P: create for all selected targets P->>P: has_symlink_component rejects agent source rect rgb(255, 247, 200) P->>D: warn with lexical path and real-file remedy end P-->>S: unchanged authorized source paths S->>A: integrate for each selected target with plan A-->>S: unchanged deployed files S-->>C: integration result C->>D: render deferred summary Note over P,A: No authorization or deployment expansionTrade-offs
AgentIntegrator.find_agent_filescan enumerate/read candidates under a symlinked agents root before authorization filtering. Empty deployed-file assertions do not prove absence of reads. Routing discovery through the canonical authorized source set, with a no-read regression, is a separate follow-up outside the expressly approved diagnostic-only scope. No duplicate detector was added here.agents/layout, README edit, normative spec edit, transport change or lockfile expansion.apm_modules; they do not exercise network acquisition.Benefits
Validation
Hosted CI is green at
aa95cf98543492cfe4d83d325659e456138409ff, with no retry on this head: CI, spec conformance, aggregate gate. All 18 checks concluded SUCCESS, NEUTRAL or SKIPPED. GitHub reports MERGEABLE / BLOCKED; passing checks and the advisory panel are not policy approval or merge permission.Two full panel passes selected and verified the wording/docs folds. Final quality stance is
ship_with_followups, solely for the separate legacy-discovery hardening noted above. Copilot inline 3973081759 was answered in-thread and resolved with that scope distinction. The suppressed Windows runtime-skip suggestion was declined under repository test conventions and observed Windows success.Local validation and mutation evidence
Current-head command:
uv run --frozen --extra dev pytest --no-cov -q \ tests/unit/install/test_agent_symlink_diagnostics.py \ tests/spec_conformance/test_manifest_reqs.py::test_agent_skip_diagnostic_preserves_authorized_scan_and_materialization \ tests/integration/test_architecture_owner_rule_mutations.py \ -k 'symlink or source_plan or source-plan'Related diagnostic, security-scan-scope, local-copy and paired conformance cases after the fold:
All seven current canonical lint gates plus architecture boundaries passed before push. Architecture boundaries also passed again at the exact pushed head. The three grep/awk guards used equivalent macOS-compatible expressions.
Historical FIX-wave evidence, not substituted for current-head execution: 315 targeted cases passed; quality suite 63 passed; full conformance 204 passed/2 pre-existing skips; 122 requirement anchors aligned. Initial Mode B failure was fixed with genuine
req-sc-015test coverage and regenerated statements, not a waiver.At previous head
068766eef4c16330716d739f5baab7823bd77a5d, CI run 34406073853 hit one Linux Shard 2 HTTP-thread semaphore timeout (job102649234979). One failed-job retry passed without code changes; stale aggregate 34406073846 was refreshed once. The same HTTP test passed immediately after all new diagnostics in one process (21 passed in 1.51s). That provenance is retained; current-head runs passed without retries.Scenario Evidence
These are real-filesystem/command component tests, not mocked security decisions.
tests/unit/install/test_agent_symlink_diagnostics.py::test_own_project_cli_reports_symlink_skip_on_install_and_reinstalltests/unit/install/test_agent_symlink_diagnostics.py::test_agent_source_skip_is_actionable_without_deployment_expansiontests/unit/install/test_agent_symlink_diagnostics.py::test_plan_reports_rejected_links_without_traversing_themtests/unit/install/test_agent_symlink_diagnostics.py::test_agent_skip_diagnostics_are_target_gated_and_printabletests/spec_conformance/test_manifest_reqs.py::test_agent_skip_diagnostic_preserves_authorized_scan_and_materialization(req-sc-015)How to test
uv run --frozen --extra dev pytest --no-cov -q tests/unit/install/test_agent_symlink_diagnostics.py; all twenty cases should pass..apm/agents -> ../agentsown-project layout and runapm install --target claude; expect the source-path warning and no agent deployment.--verbose, then replace the link with real agent files under.apm/agents/; expect the warning to disappear and the agent to deploy.uv run --frozen --extra dev pytest --no-cov -q tests/unit/install/test_security_scan_scope.py tests/unit/install/test_local_content_symlink_deref.pyto confirm existing exclusions and local-copy behavior.Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com