Skip to content

fix(install): deploy skills through home directory aliases - #2876

Open
Daniel Meppiel (danielmeppiel) wants to merge 10 commits into
danielmeppiel-bug-assessmentfrom
danielmeppiel-fix-symlink-home-deployment
Open

fix(install): deploy skills through home directory aliases#2876
Daniel Meppiel (danielmeppiel) wants to merge 10 commits into
danielmeppiel-bug-assessmentfrom
danielmeppiel-fix-symlink-home-deployment

Conversation

@danielmeppiel

@danielmeppiel Daniel Meppiel (danielmeppiel) commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

fix(install): complete skill deployment through home aliases across the lifecycle

TL;DR

Global skills now survive install, reinstall, update, and repair when HOME or
APM_HOME is a directory alias. The connected revision-A/B lifecycle exposed
additional ownership, canonical-module-path, frozen-identity, and no-lock
freshness bugs, which this PR fixes rather than limiting the claim to install.
Real generated models now execute project, global-canonical, and global-aliased variants.

Closes #2867. Thanks to Dave Mead (@DaveMeadAdjust) for the reproduction and proposed fix.

Important

Keep this PR draft until the parent reviews the full obligations and hosted CI.
Local evidence below does not claim that hosted CI is green at the new head.
Native lifecycle-ledger inheritance and fresh provider acceptance remain pending.
No new flags, relaxed descendant-link checks, or global find support are introduced.

Problem (WHY)

  • Alias-root install exited zero while producing empty skill directories.
  • Global update read ownership from the deployment root, skipped owned skills
    as collisions, and then removed them during cleanup.
  • Alias-named module directories diverged from canonical module paths;
    updating deployed skills could leave compilation reading revision A.
  • Frozen install accepted changed declared refs, while no-lock reinstall
    after removal could reuse stale mutable-ref cache state.

These are observed command/state failures, not hypothetical coverage claims.
The regression and mutation loop follows Agent Skills:
"do the work, run a validator (a script, a reference checklist, or a self-check), fix any issues, and repeat until validation passes."

Approach (WHAT)

  • Preserve plugin admission first; shallow-copy metadata and resolve its root only.
  • Read user-scope ownership from APM_HOME, independently of external deployment roots.
  • Reuse DependencyReference.get_install_path() in download and integration.
  • Compare frozen declarations with locked identity; require current upstream
    mutable refs when there is no lock to replay.
  • Assert durable state and open-world write bounds after connected commands,
    and run a mandatory transition spine inside each actual Hypothesis execution.

Implementation (HOW)

Files Intent
src/apm_cli/integration/skill_integrator.py Keep root-only normalization and propagate the correct ownership lock root across skill layouts.
src/apm_cli/install/phases/download.py, integrate.py Materialize aliases at the same canonical module location used by resolution and compilation.
src/apm_cli/install/plan.py, src/apm_cli/deps/tiered_ref_resolver.py Validate frozen source/ref/transport identity and select upstream freshness for no-lock installs.
tests/integration/test_required_lifecycle_state_machine.py Connected A/B trace for both HOME spellings and Claude/Hermes roots, hidden readers, SBOM exports, cache/source maintenance, faults and removal closure. Legacy-cache fixtures use canonical paths and assert actual rejection/removal branches followed by recovery.
tests/integration/test_generated_lifecycle_state_machine.py Three explicit real model variants, mandatory replay inside Hypothesis, bounded generated tails, global updates/uninstall, and project user-file preservation.
tests/utils/lifecycle_state.py, tests/integration/test_lifecycle_state_snapshot_contract.py Observe legacy absolute global ownership rows only inside explicitly authorized roots; retain raw records and reject escaping ancestors.
tests/integration/test_ownership_invariant_lifecycle.py, test_ref_freshness_lifecycle.py Trap update deletion/stale compilation and omitted-ref cold-cache frozen replay.
tests/unit/install/test_plan.py, test_frozen.py, tests/unit/deps/test_tiered_ref_resolver.py, tests/integration/test_frozen_host_qualified_git_e2e.py Ref/pin/transport identity and freshness cases; align handwritten locks with declared refs and semver constraints.
tests/integration/test_global_audit_deploy_root.py, tests/unit/install/test_security_scan_scope.py Original global-root and four-layout authorization regressions remain covered.
.apm/architecture/owners/contracts-tooling.json, scripts/architecture_linter/checks/contracts_test_taxonomy.py, install_base_integrator_and_contraction.py Extend existing identity-owner selectors and identity/provenance consumer guards; no sibling runtime authority.
tests/integration/test_architecture_dependency_reference.py, test_architecture_skill_provenance_scope.py Mutation-trap raw alias paths, bypassed frozen identity helpers, wrong ownership root, and missing ownership-root forwarding.
tests/spec_conformance/test_lockfile_reqs.py Bind frozen full-SHA equality/refusal to existing req-lk-003, without a refactor waiver or a new normative requirement.
CONFORMANCE.json, CONFORMANCE.md Regenerate the test inventory for that existing requirement; deterministic regeneration leaves no diff.
docs/src/content/docs/reference/cli/install.md, packages/apm-guide/.apm/skills/apm-usage/commands.md, CHANGELOG.md Document alias behavior, frozen/freshness semantics, and original contributor credit; README unchanged.

Diagrams

The connected trace advances installed state only on materializing transitions; the note identifies the newly asserted revision boundary.

stateDiagram-v2
    direction LR
    [*] --> Empty
    Empty --> InstalledA: install A
    InstalledA --> InstalledA: reinstall / compile / lock
    InstalledA --> RemoteB: publish B
    RemoteB --> RemoteB: outdated / frozen replay A
    RemoteB --> InstalledB: update B / compile B
    note right of InstalledB
        NEW: bytes, refs and ownership advance together
    end note
    InstalledB --> InstalledB: frozen replay / drift refusal / reinstall
    InstalledB --> Faulted: inject audit faults
    Faulted --> InstalledB: restore / repair / audit
    InstalledB --> Removed: uninstall / audit
    Removed --> InstalledB: redeclare / install B
    Removed --> [*]
Loading

Trade-offs

  • Root normalization, not candidate resolution. Descendant links and source-plan filters stay authoritative; caller metadata remains unchanged.
  • Compatibility observer, not lock schema migration. Legacy absolute global deployment rows are bounded against independently supplied roots without resolving candidate descendants.
  • Actual command semantics, not universal atomicity. After a deliberately broken update advances the lock but loses deployment, another update is a no-op; install repairs it. Project prune preserves modified user files, with explicit collision/force-repair coverage.
  • Bounded variants, not a Cartesian product. Project Copilot retains SHA-pinned behavior; global canonical/aliased models use a mutable branch and explicit instruction compilation. Symlink cases skip only where directory symlinks are unavailable.
  • Scope is explicit. Project-only commands are mapped below, not assigned invented global flags; executable trust, script execution, registry publishing, and CLI self-update are different resources from this passive Git skill/instruction lifecycle.

Benefits

  1. Both HOME spellings deploy revision B, record its ref/hash/ownership, and compile B rather than A.
  2. Reinstall converges byte-for-byte; frozen replay/refusal, fault repair, and removal/reinstallation have durable-state assertions.
  3. Every generated variant executes mandatory rules before its bounded random tail; randomness cannot omit the alias variant or the A/B update spine.
  4. Physical HOME, caller workspace, and disjoint external roots retain sentinels and user content; aliases themselves remain unchanged.

Validation

Merged current main at 1cab81dc6; final pushed head is 171a3ecbe.
Only generated conformance statements changed after 7ae103536; source, tests,
scripts and owner registry are byte-identical. At 7ae103536, all eight deterministic/generated lifecycle witnesses,
their rule catalog, lockfile conformance, and the merged Windows contract tests:

142 passed, 1 skipped in 615.13s (0:10:15)

The skip is the existing publisher-timestamp SHOULD waiver, not a lifecycle
variant or the new frozen full-SHA test. Before the unrelated Windows-test-only
main merge, the complete required lifecycle family, generated family, lockfile
conformance and new owner guards at 1f917a150 passed:

75 passed, 1 skipped in 742.98s (0:12:22)

Full spec suite at 171a3ecbe: 203 passed, 2 skipped in 21.88s.
Both skips are existing specification waivers; the new test passes.
Conformance statements regenerate with no diff. This repairs the sole failing
hosted check at 7ae103536; all other checks at that predecessor succeeded.

Full canonical lint at 171a3ecbe: Ruff check/format, YAML I/O, 2100-line limit,
portable-relative-path guard, pylint R0801, auth boundaries, and architecture
boundaries all exited zero. Mode B recognizes the real req-lk-003 conformance
test; orphan checking aligns all 122 requirements. Assertion and exact-duplicate
ratchets pass. uv.lock is unchanged. mmdc rendered the diagram.

The authored native contract assesses all 84 Click registrations individually
(22 applicable, including hidden info and lock export), with eight exact
variant witnesses and command-specific resource reasons for the rest. An actual
native-provider run passed all eight pytest nodes; corrected authored contexts
match its recorded trajectories. That diagnostic is not a fresh acceptance
receipt: provider integration/rerun and current hosted CI are still pending.

Executed transition matrix and command applicability

G is the physical APM_HOME; command environment retains the lexical HOME/APM_HOME
alias in the aliased case. I means
install --global --no-policy --parallel-downloads 0.

Commands / transitions Observed state contract
I A; repeat I A Real skill/instruction bytes, canonical module location, ownership; second install byte-converges.
compile --global A; lock --global --no-policy --parallel-downloads 0 Compiled A exists; locking retains deployed files and ownership.
lock export --global A; lock export --global --format spdx B Real CycloneDX/SPDX output contains the corresponding installed commit; exports leave installed state unchanged. Generated global variants exercise both formats at both revisions.
Publish B in Git worktree; outdated --global --parallel-checks 0 --verbose B is reported while installed durable state stays A; cache effects are separate.
update --global --dry-run --parallel-downloads 0; I --frozen Preview does not deploy B; locked A replays after upstream advances.
update --global --yes --parallel-downloads 0; compile --global Ref, hashes, ownership, skills and compiled content advance to B.
compile --global --dry-run; compile again; I --dry-run; I --update; deps update --global --parallel-downloads 0 Supported previews/no-ops retain B byte-for-byte.
lock --global --no-policy --parallel-downloads 0 --update; I --frozen B remains stable; after intentionally changing declared ref, frozen exits 1 unchanged; restore declaration and reinstall B converges.
audit --ci --no-policy --no-fail-fast --format json, cwd G Clean B, missing/edited skill, missing modules, corrupt ownership, escaping symlink, combined faults, and restored B; audit is read-only.
cache clean --yes; deps clean --dry-run; deps clean --yes, cwd G; audit; I Disposable cache removal preserves installed B; source preview unchanged; actual source removal yields audit failure while deployments survive; install rehydrates and clean audit recovers.
uninstall --global <remote_url>; audit; redeclare; I; audit; uninstall; audit Owned removal, empty audit, fresh B reinstallation, and final clean removal closure; sentinels survive.
deps list/tree/why --global; view <name> --global; hidden info <name> --global Installed package is reported without durable mutation.
deps info <name>, cwd G Legacy installed-metadata alias reports the package; no invented global option.
cache info; cache prune --days 30 Statistics and no-op pruning preserve installed state, with cache bookkeeping allowed. Required trace invokes these from G; generated variants use the caller.
targets --json, cwd caller Project-marker inspection returns no active targets in the empty caller; it is not a global deployment inventory.
find <absolute-global-skill-path>, cwd G Existing project-relative lookup exits 1 unchanged for legacy absolute global records. No claim of successful global lookup.
prune Applicable to project declaration removal; generated project model covers removal, retained modified user content, re-add collision, and force repair. Global removal uses uninstall.
approve, deny, policy, lifecycle Executable grants/policy and lifecycle-script resources are absent from this passive skill/instruction fixture. They are not deployment/update aliases.
init, pack, unpack, publish, plugin Authoring/distribution commands operate on a producer project or artifact. Revision publishing here uses the real local Git fixture; no registry or bundle is declared.
marketplace, search, mcp, remote-version view Separate discovery/service/registry resources; this fixture declares a direct Git package, not those resources.
list, preview, run, runtime, doctor, self-update, config, experimental Script/runtime/host diagnostics, CLI-binary updates, and preference/feature configuration do not transition this installed passive dependency. No scripts, alternate binaries, or feature changes are declared.
Generated execution and mutation evidence

test_generated_lifecycle_sequences_preserve_reference_model passes separately
for project, global-canonical, and global-aliased.
run_state_machine_as_test uses bounded settings: 6 project / 3 each global
examples, 8 tail steps, deterministic generation/shrinking. The initializer
executes _mandatory_replay inside the actual Hypothesis run, not just a separate test.
The separate three mandatory-replay cases and exact rule/property catalog also pass.

Global spine: dry-run, install, reinstall, compile, lock, installed readers,
both lock exports, publish, outdated, frozen A, update, compile B, both lock exports,
legacy update, frozen B, frozen refusal, reinstall, cache clean,
source clean/audit failure/rehydration, tamper,
audit-tampered, repair, audit-clean, uninstall, audit-empty, redeclare,
install B, audit-clean, uninstall, audit-empty. Project rules retain declaration
removal/prune and add modified-user-file collision/force-repair closure.

Deliberate mutation Actual failure Restored outcome
Remove only package-root .resolve() Independent aliased first-install, missing-file reinstall, and A-to-B update all exit 0 but fail: skill_exists=False. Fresh controls for all three pass; install repairs all mutant residues.
Make update freshness reproducible/stale Focused global update test fails with revision-A skill bytes instead of B. Included in the 516-pass restored suite.
Treat any existing compiled file as unchanged Actual aliased mandatory replay fails expecting # revision-b while compiled output still contains # revision-a. Included in the 516-pass restored suite.

No mutations remain in the committed source.

Scenario Evidence

# Scenario (user promise) Principle(s) Test(s) proving it Type
1 Install, update, compile, repair and remove through home aliases DevX (pragmatic as npm), Governed by policy tests/integration/test_required_lifecycle_state_machine.py::test_required_global_audit_rule_matrix_for_external_roots (regression-trap for #2867) e2e
2 Command sequences cannot skip global alias/update behavior Portability by manifest, Governed by policy tests/integration/test_generated_lifecycle_state_machine.py::test_generated_lifecycle_sequences_preserve_reference_model e2e
3 Updating owned external skills preserves them and compiles the new instruction Multi-harness support, DevX (pragmatic as npm) tests/integration/test_ownership_invariant_lifecycle.py::test_global_update_preserves_owned_external_skill_targets e2e
4 Frozen replay recovers a cold omitted-ref installation without taking upstream B Governed by policy tests/integration/test_ref_freshness_lifecycle.py::test_frozen_default_ref_rehydrates_cold_cache_without_ref_drift e2e
5 Root aliases do not authorize package links or mutate caller metadata Secure by default tests/unit/install/test_security_scan_scope.py::test_package_skill_root_alias_preserves_authorization, tests/integration/test_lifecycle_state_snapshot_contract.py integration
6 Frozen declarations must match the exact locked commit Governed by policy tests/spec_conformance/test_lockfile_reqs.py::test_frozen_manifest_pin_requires_the_exact_locked_commit (req-lk-003) component

How to test

  • Set APM_E2E_TESTS=1 APM_BINARY_PATH="$PWD/.venv/bin/apm" and run uv run --extra dev pytest -q tests/integration/test_generated_lifecycle_state_machine.py tests/integration/test_required_lifecycle_state_machine.py::test_required_global_audit_rule_matrix_for_external_roots.
  • Run the ownership, ref-freshness, frozen-host, snapshot, plan/frozen/tiered-resolver, and existing skill-containment tests listed above.
  • Repeat a bounded mutation from the table; require the corresponding byte/ref assertion to fail, then restore source and rerun.
  • Inspect current hosted CI and the complete lifecycle obligations before removing draft status; earlier-head green checks are not acceptance evidence for this head.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Normalize the skill source root on a shallow metadata copy while preserving source-plan authorization and descendant symlink rejection. Add global audit and skill layout regression coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new CHANGELOG entry references the closed issue number instead of ending with the PR number per the repo’s changelog format contract.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 2 Low severity

New issues introduced by this change (2)
Severity Finding
Low severity CHANGELOG.md — The new changelog bullet ends with #2867, but changelog entries are required to end with the pull…
Low severity tests/​unit/​install/​test_security_scan_scope.py — In the MARKETPLACE_PLUGIN branch the test writes .claude-plugin/plugin.json, but the subsequent…
What changed in this PR

Fixes a global skill-install edge case where deploying skill files could be silently skipped when HOME/APM_HOME is a directory-symlink alias, by canonicalizing the package root spelling used during skill integration while preserving existing authorization and descendant-link protections.

Changes:

  • Canonicalize PackageInfo.install_path on a shallow copy inside SkillIntegrator.integrate_package_skill() to align with DeployableSourcePlan root resolution.
  • Add regression coverage for aliased home paths in the global install/audit integration test and add a multi-layout unit/component test ensuring authorization is preserved under root aliases.
  • Document the root-alias behavior in CLI install docs and add an Unreleased changelog entry.
File Description
src/​apm_cli/​integration/​skill_integrator.py Shallow-copies package metadata and resolves the package root before skill routing/copy so source-plan authorization matches copy/discovery paths.
tests/​unit/​install/​test_security_scan_scope.py Adds a multi-layout test asserting root-alias normalization does not broaden authorization and still rejects symlink escapes.
tests/​integration/​test_global_audit_deploy_root.py Adds a parametric regression test for global install + audit when HOME/APM_HOME uses a directory symlink alias.
docs/​src/​content/​docs/​reference/​cli/​install.md Documents that global skill installation supports home directory alias spellings while keeping in-package symlink rejection unchanged.
CHANGELOG.md Records the fix under Unreleased.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CHANGELOG.md Outdated
Comment thread tests/unit/install/test_security_scan_scope.py Outdated
@danielmeppiel

Daniel Meppiel (danielmeppiel) commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

PR #2876 restores actual global skill content through HOME/APM_HOME directory aliases while preserving source authorization; both prior follow-ups are closed at 70e52a0.

cc Sergio Sisternes (@sergio-sisternes-epam) -- a fresh advisory pass is ready for your review.

The eight active specialists converge with no substantive dissent; auth is inactive because its conditional probe found no authentication changes. Both strategic reservations are satisfied: (1) root-only normalization preserves descendant symlink restrictions, source authorization, and escape rejection without broadening trusted paths; (2) CHANGELOG and the install reference explicitly document HOME/APM_HOME root-alias handling with no new flags or required environment changes. The inspected diff retains admission first, then shallow-copies metadata and resolves only the local package root, leaving caller metadata and existing routing intact. The deterministic classification remains owner-extension, with dual_guardrail_required=false: ownership is unchanged, no authority is centralized, and the existing admission-first static guard remains applicable.

Both prior FOLD items are present at the verified local HEAD: explicit normalize_plugin_directory(source, plugin_json_path=manifest), and canonical/aliased HOME/APM_HOME parameterization of tests/integration/test_required_lifecycle_state_machine.py::test_required_global_audit_rule_matrix_for_external_roots. The coverage specialist inspected its real-binary install/audit/fault/restore/uninstall state assertions. Four-layout filesystem coverage in tests/unit/install/test_security_scan_scope.py::test_package_skill_root_alias_preserves_authorization includes the exact assertion assert (deployed / "SKILL.md").read_text(encoding="utf-8") == "# Demo\n", alongside caller-metadata preservation and descendant-link exclusion; directory placement does not diminish this real-filesystem evidence. Supplied driver results report 345 passed with 3 subtests, six focused passes, and one owner-test pass. Removing production root resolution produced the expected missing-SKILL.md lifecycle failure; the assignment is restored at this HEAD. Aggregate logs lack embedded commands and HEAD identifiers, so their exact-head attribution rests on supplied driver context; functional-evidence.json explicitly ties the owner test to this HEAD. Neither specialists nor this synthesis reran tests, and this is not fresh independent certification or a claim of hosted CI or canonical lint success at the new head.

Design patterns: retain the localized copy-and-normalize implementation within the existing BaseIntegrator/SkillIntegrator relationship and the unchanged frozen DeployableSourcePlan authorization value object. The architect's sole informational nit supports the current design rather than requesting another abstraction. The active performance specialist finds bounded per-invocation work: one shallow metadata copy and one root resolution whose cost follows path depth and symlink expansion, with no new per-file loop or application-level network requests. Filesystem latency remains environment-dependent; correctness-test timings support no speed claim. Nothing in this final convergence pass warrants expanding scope.

Aligned with: Portable by manifest, Secure by default, Governed by policy, OSS community-driven, Pragmatic as npm

Growth signal. Frame this as a contributor-backed reliability correction: global installs deploy authorized skill content through existing home-directory aliases, without setup changes or relaxed package trust. No broader adoption or performance claim is needed.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 1 No substantive architectural concern. Root-only normalization preserves caller metadata, admission ordering, and existing source-plan authorization.
CLI Logging Expert 0 0 0 No CLI logging concerns: root-alias deployment is corrected without changing terminal output, flags, or environment configuration.
DevX UX Expert 0 0 0 Alias handling restores expected global install behavior without new flags or environment migration; both prior folds are present. No remaining DevX findings.
Supply Chain Security 0 0 0 Root-only normalization preserves source authorization, descendant-link rejection, caller metadata, and admission-first ordering. Both prior folds are present.
OSS Growth Hacker 0 0 0 Both reservations are satisfied: root aliases retain descendant/source restrictions, and release/install docs name HOME/APM_HOME support without flags or migration, with contributor credit.
Doc Writer 0 0 0 Install reference and CHANGELOG accurately describe root-alias support without implying broader package trust or environment migration. No remaining documentation findings.
Test Coverage 0 0 0 Both requested folds are present; filesystem and real-CLI lifecycle regression coverage address the alias fix. No remaining coverage gap found.
Performance Expert 0 0 0 Root-only normalization adds bounded metadata copying and path traversal per package, not per file. No actionable performance regression in the final six-file diff.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Architecture

classDiagram
    direction LR
    class BaseIntegrator {
      <<BaseClass>>
    }
    class SkillIntegrator {
      <<ConcreteIntegrator>>
      +integrate_package_skill()
      +skill_source_paths()
      -_build_deployable_copy_ignore()
    }
    class PackageInfo {
      <<MutableDataclass>>
      +install_path Path
    }
    class DeployableSourcePlan {
      <<ValueObject>>
      +create()
      +copy_ignore()
    }
    class SkillIntegrationResult {
      <<Dataclass>>
      +target_paths list
    }
    class services {
      <<Module>>
      +integrate_package_primitives()
    }
    class errors {
      <<AdmissionBoundary>>
      +enforce_agent_plugin_deployment_boundary()
    }
    BaseIntegrator <|-- SkillIntegrator
    services ..> SkillIntegrator : dispatches
    services ..> DeployableSourcePlan : creates
    SkillIntegrator ..> errors : admission first
    SkillIntegrator ..> PackageInfo : shallow copy and root resolve
    SkillIntegrator ..> DeployableSourcePlan : consumes authorization
    SkillIntegrator ..> SkillIntegrationResult : returns
    note for SkillIntegrator "Base + subclass: existing specialized materializer"
    note for DeployableSourcePlan "Dataclass-as-value-object: frozen source authorization plan"
    note for PackageInfo "Only copied install_path changes; caller metadata is preserved"
    class SkillIntegrator:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    CLI["[I/O] apm install --global<br/>src/apm_cli/commands/install.py: install()"]
    PIPE["[I/O] Existing install pipeline<br/>install/services.py: integrate_package_primitives()<br/>DeployableSourcePlan.create() and pre-deploy scan"]
    ADMIT{"integration/skill_integrator.py: integrate_package_skill()<br/>enforce_agent_plugin_deployment_boundary() FIRST"}
    ERROR["agent_plugins/errors.py:<br/>admission exception before normalization"]
    ROOT["[I/O] copy(package_info)<br/>copied install_path = install_path.resolve()<br/>caller metadata unchanged"]
    ROUTE["[I/O] Unchanged routing and discovery<br/>should_install_skill(), skill_source_paths()<br/>native, bundle, and standalone sub-skill paths"]
    COPY["[FS] Existing materialization: shutil.copytree()<br/>_build_deployable_copy_ignore() callback"]
    AUTH{"[I/O] install/deployable_source_plan.py:<br/>DeployableSourcePlan.copy_ignore()<br/>_is_safe_source_path() and authorized membership"}
    DENY["Ignore candidate: descendant symlink,<br/>containment escape, or absent authorization"]
    PAYLOAD["[FS] Copy authorized SKILL.md and safe payload<br/>return SkillIntegrationResult.target_paths"]
    DONE["commands/install.py: apply_install_command_outcome()<br/>ctx.exit(outcome.exit_code)<br/>tested successful global install: 0"]
    CLI --> PIPE
    PIPE -- existing approved skill path --> ADMIT
    ADMIT -- denied --> ERROR
    ADMIT -- admitted --> ROOT --> ROUTE
    ROUTE -- deployable skills --> COPY --> AUTH
    ROUTE -- existing skip or no-source result --> DONE
    AUTH -- unsafe or unauthorized --> DENY
    AUTH -- safe and authorized --> PAYLOAD
    PAYLOAD -. remaining pipeline and finalization .-> DONE
Loading

Recommendation

I recommend shipping this bounded correction for maintainer consideration. Both earlier follow-ups are closed, and the final specialist returns identify no remaining actionable work. The parent retains the planned published-evidence refresh and hosted-CI verification; this recommendation does not certify either.


Full per-persona findings

Python Architect

  • [nit] Keep the localized copy-and-normalize design; no additional abstraction is needed. at src/apm_cli/integration/skill_integrator.py:1465
    Design patterns
  • Used in this PR: Base + subclass -- SkillIntegrator retains BaseIntegrator collaboration while normalizing the package root once before skill-layout dispatch.
  • Used in this PR: Dataclass-as-value-object -- the unchanged frozen DeployableSourcePlan remains the authorization input; shallow-copying mutable PackageInfo isolates the new install_path assignment from its caller.
  • Pragmatic suggestion: none -- the current shape is the simplest correct design at this scope.

CLI Logging Expert

No findings.

DevX UX Expert

No findings.

Supply Chain Security

No findings.

OSS Growth Hacker

No findings.

Auth Expert -- inactive

Diff touches src/apm_cli/integration/skill_integrator.py (root-only copy/resolve) plus CHANGELOG and install docs and three tests; no changes to token management, AuthResolver, HostInfo, AuthContext, credential helpers, or remote host authentication.

Doc Writer

No findings.

Test Coverage

No findings.

Performance Expert

No findings.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

Addresses the panel lifecycle follow-up and Copilot plugin fixture comment. Run global install and audit through the installed CLI with aliased HOME/APM_HOME while preserving physical-root snapshots and user-owned sentinels; pass the written plugin manifest explicitly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Shepherd driver: converged for maintainer review

The final full panel recommends ship_now at 70e52a03dc40b971fed2e5e94c10e7c72287b011. No actionable follow-ups remain. The single panel recommendation was updated in place above.

Reservations carried from strategic-alignment

  • Preserve descendant symlink and source-authorization checks; verify alias-path deployment and escape rejection without broadening trusted paths. -- Addressed by root-only normalization after plugin admission, unchanged source-plan authorization, and real-filesystem descendant-link/escape assertions.
  • Name and document root-alias handling in CHANGELOG; require no new flags or changes to user HOME/APM_HOME. -- Addressed by the Unreleased entry and install reference note; both home spellings work with unchanged commands.

Folded in this run

Copilot signals reviewed

  • CHANGELOG.md -- LEGIT: changelog trailer requires the PR number; corrected and thread resolved.
  • tests/unit/install/test_security_scan_scope.py -- LEGIT: normalization needs the explicit non-root manifest path; corrected and thread resolved.
  • Second fetch found no additional Copilot findings.

Regression-trap evidence (mutation-break gate)

  • Removed only the root-resolution assignment: aliased global install and three non-plugin layout cases failed; canonical home and parser-normalized plugin controls passed.
  • Removed the same assignment for the added real-CLI alias lifecycle case: missing SKILL.md caused the expected failure. Assignment restored before the final suite.
  • Final-head suite: 345 passed, 3 subtests passed in 30.21s; separately executed declared-plugin owner case: 1 passed in 0.69s.

Canonical-owner evidence

Exact base/head detection reports the legacy plugin membership/placeholder owner through the integrator selector. Classification: owner-extension, no centralization or authority rerouting, dual_guardrail_required=false. Exact-head executed declared-plugin functional evidence and clean admission-first boundary lint cover the detected owner. The version 2 completion and semantic owner verifier passed.

Lint contract

The complete current canonical mirror passed before push: Ruff check/format on runtime, tests and architecture scripts; YAML I/O, 2100-line and portable-path guards; pylint R0801; auth signals and architecture boundaries.

CI

Final-head CI passed, including both Linux shards, Windows Compatibility, lifecycle, binary smoke, architecture ratchets and lint. Merge Gate also passed. Full rollup: 17 successful checks and one intentional docs-deploy skip; zero CI recovery iterations.

Mergeability status

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#2876 70e52a0 ship_now 1 3 0 2 green MERGEABLE BLOCKED awaiting maintainer/protection requirements

Convergence

One outer iteration; two Copilot rounds; full initial and final specialist panels. No deferrals. This is a landing-ready advisory, not an approval or merge. No issue closure or merge was performed.

@danielmeppiel
Daniel Meppiel (danielmeppiel) marked this pull request as draft September 7, 2026 14:41
Exercise connected global canonical and aliased command traces and deterministic spines inside generated models. Preserve global skill ownership and canonical module paths across update, validate frozen identity, and refresh mutable refs when no lock exists.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…imits

Exercise deps info, cache info/prune, project target inspection, and global absolute-file find refusal without inventing unsupported flags or weakening durable-state checks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Docs sync advisory

Verdict: no residual docs changes * Pages affected: 2 already updated * LLM calls: 3/15

At 9eeaf3785d06c70238628ca55dfbc7c81e57e79d, the install reference and packaged command guide cover alias-root ownership, canonical module materialization, frozen declared-identity checks, and no-lock mutable-ref freshness. The index-only classifier nominated compile/lock/outdated/update references; page-level localization found those pages already truthful, and CDO agreed. No structural change or companion PR is needed. This is advisory and does not change draft status or claim hosted CI acceptance.

Bind frozen full-SHA validation to req-lk-003 and extend canonical/aliased trajectories with real lock exports and cache/source maintenance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extend the existing identity owner selectors and provenance consumer checks for canonical module paths, frozen drift validation, and user-scope lockfile roots, with focused bypass mutations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Regenerate the conformance statements after adding the req-lk-003 frozen full-SHA equality/refusal test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Daniel Meppiel (danielmeppiel) changed the base branch from main to danielmeppiel-bug-assessment September 7, 2026 18:52
@danielmeppiel
Daniel Meppiel (danielmeppiel) marked this pull request as ready for review September 8, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Global install succeeds with empty skill directories when HOME/APM_HOME uses a symlink alias

2 participants