feat(claude-code): auto-discover plugin marketplace agents#231
feat(claude-code): auto-discover plugin marketplace agents#231leih1219 wants to merge 11 commits into
Conversation
Agents enabled via Claude Code plugin marketplaces (~/.claude/settings.json → extraKnownMarketplaces) are now discoverable by CAO without requiring a manual agent_dirs.claude_code entry in ~/.aws/cli-agent-orchestrator/settings.json. Changes: - utils/agent_profiles.py: add _discover_claude_plugin_agent_dirs() that walks enabled marketplaces, validates marketplace.json, and collects each plugin's agents/ directory. Integrated into list_agent_profiles() and _read_agent_profile_source() scan order. - services/settings_service.py: change default agent_dirs.claude_code from ~/.aws/cli-agent-orchestrator/agent-store to ~/.claude/agents/. Users with a saved value are unaffected (saved-over-default merge semantics). - docs/settings.md: document the new discovery behavior. - test/utils/test_claude_plugin_discovery.py: unit tests covering happy path, empty enabledPlugins, orphan plugin entries, file-vs-dir source validation, cross-marketplace name collision, symlink escape, and _read_agent_profile_source() plugin integration. - CHANGELOG.md: Unreleased entry noting discovery + default path change.
|
@patricka3125 would you like to review this if you get a chance ? |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #231 +/- ##
=======================================
Coverage ? 92.73%
=======================================
Files ? 65
Lines ? 5548
Branches ? 0
=======================================
Hits ? 5145
Misses ? 403
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…arsing Some profile generators (e.g. AIM `plugins install --local`) prepend <!-- ... --> HTML comment blocks before the YAML frontmatter delimiter. python-frontmatter requires '---' on the first line, so these profiles silently parse with empty metadata — causing mcpServers, model, and allowedTools to be None at runtime. Add a defensive regex strip at the top of parse_agent_profile_text() so CAO handles these profiles correctly regardless of upstream generator behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| plugin_dir = (mkt_path / plugin_source).resolve() | ||
| # Path containment check | ||
| try: | ||
| plugin_dir.relative_to(resolved_mkt) |
There was a problem hiding this comment.
Individual files still could be outside. Probably should validate individual files too.
There was a problem hiding this comment.
Addressed in b43a1d2 — added a _scan_plugin_directory wrapper that resolves each candidate file and validates resolve().relative_to(plugin_root) before adding it. Scoped narrowly to the claude_plugin discovery path (not broadening _scan_directory, so other agent sources keep their existing behavior).
Tests covering this:
test_symlink_outside_plugin_root_rejected— symlink insideagents/pointing outside plugin root is skipped with a warningtest_symlink_within_plugin_root_accepted— symlinks resolving within the plugin root still worktest_read_agent_profile_source_rejects_symlink_escape— covers the second call site end-to-endTestFixAScopeIsNarrow::test_symlink_escape_in_non_plugin_dir_not_blocked— regression guard so the narrow-scope decision is pinned
| # Strip leading HTML comments before the YAML frontmatter fence. | ||
| # Some profile generators (e.g. AIM) prepend <!-- ... --> blocks that | ||
| # prevent python-frontmatter from detecting the opening '---' delimiter. | ||
| resolved_text = re.sub(r"^<!--.*?-->\s*", "", resolved_text, flags=re.DOTALL) |
There was a problem hiding this comment.
HTML comment stripping regex only removes a single leading comment block. If a generator produces multiple consecutive comments (e.g., ), only the first is stripped and the second still blocks frontmatter detection.
There was a problem hiding this comment.
Addressed in 6a93db3 — changed the regex from ^<!--.*?-->\s* to ^(?:<!--.*?-->\s*)+ so all consecutive leading comment blocks are stripped in a single re.sub call. Linear-time (no backtracking risk — the inner .*?--> is bounded by a literal terminator, the outer + advances sequentially).
Tests:
test_multiple_leading_html_comments_stripped— two consecutive blockstest_three_leading_html_comments_stripped— three blocks (edge case)- Existing
test_leading_html_comment_stripped_before_frontmatter/test_multiline_html_comment_stripped/test_no_comment_passthrough/test_non_leading_comment_not_strippedall still pass
anilkmr-a2z
left a comment
There was a problem hiding this comment.
Mostly minor commant except one.
|
@leih1219 can you please help to address the comments ? |
The ^-anchored regex introduced in 03e6d35 only matched the first leading HTML comment block. Profiles with multiple consecutive <!-- ... --> blocks (e.g. AIM-generated profiles with a boilerplate header + per-agent header) would have only the first stripped, causing frontmatter detection to fail. Replace the single-match pattern with a non-capturing repeat group `^(?:<!--.*?-->\s*)+` so all leading comment blocks are stripped in a single regex call.
Individual files inside a plugin's agents/ directory (e.g. symlinks pointing outside the marketplace root) were enumerated without per-file validation. Add _scan_plugin_directory() that wraps _scan_directory with resolve()+is_relative_to() checks against the marketplace root for each file entry. Scope is narrow: only the claude_plugin discovery path uses the new containment check. Other _scan_directory callers (local store, provider dirs, extra dirs) are unchanged. _discover_claude_plugin_agent_dirs() now returns (agents_dir, plugin_root) tuples so callers can pass the root for containment validation. Addresses reviewer comment about individual files escaping containment.
…invalidation _discover_claude_plugin_agent_dirs() is called on every list_agent_profiles() invocation from the long-running CAO server. Add a module-level cache keyed on the mtime of settings.json and each marketplace.json so repeated calls with unchanged filesystem state return instantly. Cache invalidation is automatic: any mtime change on settings.json triggers a full re-discovery, and mtime changes on individual marketplace.json files are also detected. Expose _reset_plugin_discovery_cache() (underscore-prefixed, test-only) so tests can isolate from each other. An autouse fixture in the test file ensures no cross-test cache pollution. Addresses reviewer comment about repeated filesystem reads on every invocation.
Tester-agent pass adding 7 new tests and 2 mock additions, filling scenario-coverage gaps surfaced by the tester task for PR awslabs#231: Fix C (HTML-comment strip): - test_three_leading_html_comments_stripped: edge-case for 3+ blocks Fix A (per-file plugin containment): - test_read_agent_profile_source_rejects_symlink_escape / accepts_regular_plugin_file: cover the second call site (_read_agent_profile_source) end-to-end - test_symlink_escape_in_non_plugin_dir_not_blocked: regression guard asserting the scope stays narrow (claude_plugin only) Fix B (discovery cache): - test_reset_plugin_discovery_cache_clears_state: explicit reset-helper test - test_cache_invalidates_when_new_marketplace_added - test_cache_invalidates_when_marketplace_json_disappears Mocks (home-dir leakage in existing tests): - test_builtin_store_exception_handled: mock _discover_claude_plugin_agent_dirs - test_non_md_builtin_files_skipped: same Coverage on agent_profiles.py: 85%% -> 92%% Impacted test count: 73 -> 80 (with the 2 mock-added tests now stable) Full suite: 1642 / 1642 non-infra tests pass.
|
Hi @leih1219 this is a really interesting PR, great work. I have some overall thoughts on the high level direction and approach of this feature that I would like to request clarification on... I think the motivation needs a sharper compatibility model before we add automatic discovery. The PR assumes that “Claude Code plugin ships agents” implies “those agents should be selectable from CAO,” but I don’t think that follows yet. A Claude plugin agent is part of a broader Claude Code plugin runtime: plugins can ship agents alongside skills, hooks, MCP/LSP configs, monitors, There is also a scoping question. Claude has distinct project, user, and plugin subagent scopes, with defined precedence. CAO has local/provider/custom/built-in profile sources and an orchestration-specific runtime model. Before scanning plugin marketplaces by default, I think we should define the relationship between:
Those are not obviously interchangeable. Their frontmatter and semantics also differ: Claude subagents use fields like Because of that, I’m not convinced automatic plugin-agent discovery is the right first step. It may make CAO list agents that look available but are degraded or incorrect when run outside the Claude plugin runtime. It also does not solve the stated expectation fully: if a plugin agent depends on plugin skills, hooks, scripts, or settings, simply pointing CAO at I would prefer one of these narrower approaches:
So I’m supportive of improving interop, but I think this PR currently conflates discoverability with compatibility. I’d like to see the compatibility/scoping model clarified before this becomes default behavior. |
Summary
Auto-discover agents from enabled Claude Code plugin marketplaces (those
declared in
~/.claude/settings.json→extraKnownMarketplacesandenabled in
enabledPlugins). Today, agents installed as part of aClaude Code plugin are invisible to CAO unless the user manually points
agent_dirs.claude_codeat the plugin'sagents/directory. This changemakes them discoverable by default.
Motivation
Claude Code is the only supported provider without an auto-populated
default agent directory.
constants.pydefines:Users who install a Claude Code plugin that ships agents (via the plugin
marketplace mechanism) expect those agents to be selectable from CAO
without further manual setup. This PR closes that gap for the
marketplace-plugin case.
What changed
src/cli_agent_orchestrator/utils/agent_profiles.py— adds_discover_claude_plugin_agent_dirs(). It reads~/.claude/settings.json, iteratesextraKnownMarketplaces, parseseach marketplace's
.claude-plugin/marketplace.json, and for eachplugin present in
enabledPlugins["<plugin>@<marketplace>"]collectsthe plugin's
agents/directory. Integrated into bothlist_agent_profiles()and_read_agent_profile_source()betweenprovider directories and extra user-added directories.
src/cli_agent_orchestrator/services/settings_service.py— changesthe default
agent_dirs.claude_codevalue from~/.aws/cli-agent-orchestrator/agent-storeto~/.claude/agents/,aligning with Claude Code's native user-level subagent directory.
Users with a saved
agent_dirs.claude_codevalue in theirsettings.jsonare unaffected becauseget_agent_dirs()merges savedvalues over defaults.
docs/settings.md— new section documenting plugin-marketplacediscovery, precedence order, how to disable via removing
enabledPlugins, and known limitations.CHANGELOG.md— Unreleased entry.Security & path safety
plugin_dir.resolve().relative_to(marketplace_root.resolve())prevents traversal across marketplace boundaries.
b43a1d2):_scan_plugin_directoryresolves each candidate file and validatesresolve().relative_to(plugin_root)before adding it. Symlinks insideagents/that point outside the plugin root are rejected with a logwarning. Scoped narrowly to the claude_plugin discovery path — other
_scan_directorycallers are untouched.settings.json,marketplace.json) are wrapped withJSONDecodeError+OSErrorhandlers. Missing or malformed filesproduce a single log warning and return an empty list.
claude_codeprovider path; otherproviders are untouched.
Round 2 review response (2026-05-12)
Three commits address @anilkmr-a2z's review feedback, plus one test-coverage commit:
6a93db3— fix(agent_profiles): strip multiple leading HTML comment blocks. Regex changed from^<!--.*?-->\s*to^(?:<!--.*?-->\s*)+so consecutive leading blocks are stripped in a single pass.b43a1d2— fix(agent_profiles): validate per-file containment for plugin agents. Adds_scan_plugin_directorywrapper withresolve() + relative_to()on each file; scope is narrow (claude_plugin only) and a regression guard test pins the scope.878f315— perf(agent_profiles): cache claude plugin discovery with mtime-based invalidation. Module-level cache keyed onsettings.jsonmtime plus eachmarketplace.jsonmtime. Automatic invalidation on any tracked-file change.22e3e92— test(agent_profiles): expand coverage for plugin discovery fixes. +7 tests and +2 mocks, pushingagent_profiles.pyline coverage from 85% → 92%.Tests
test/utils/test_claude_plugin_discovery.pynow covers:agents/directory~/.claude/settings.jsonmarketplace.jsona directory
enabledPluginsmaplist_agent_profilesintegration: plugin source label, local-beats-plugin dedup_read_agent_profile_sourceintegration: found in plugin dir, raisesFileNotFoundErrorwhen not found anywhereagents/pointing outside plugin root is rejected with a warning;symlinks resolving within the plugin root are accepted
directories (e.g.,
~/.kiro/agents/) are NOT rejected — thenarrow-scope decision is pinned
mtime changes on
settings.jsonor anymarketplace.jsonforcere-discovery; new-marketplace-added and marketplace-disappeared cases;
explicit
_reset_plugin_discovery_cache()test/utils/test_agent_profiles.pyadditionally covers multi-block HTMLcomment stripping (1, 2, and 3+ leading blocks; multi-line blocks;
non-leading comments not stripped).
Run locally:
Coverage on
agent_profiles.py: 92%.Full non-infra suite: 1642 passed / 5 skipped.
Backwards compatibility
agent_dirs.claude_codesaved in~/.aws/cli-agent-orchestrator/settings.jsonkeep their existingbehavior (saved-over-default merge).
~/.aws/cli-agent-orchestrator/agent-storeto~/.claude/agents/.Since
agent-storeis already scanned asLOCAL_AGENT_STORE_DIR,this is a no-op for agent visibility.
_discover_claude_plugin_agent_dirsreturn type changed fromList[Path]toList[Tuple[Path, Path]](adding the marketplaceroot for per-file containment). The function is underscore-prefixed
and private to
agent_profiles.py; all in-source callers areupdated.
list_agent_profiles,_read_agent_profile_source,load_agent_profile) are unchanged.Follow-ups (intentionally out of scope)
.claude/agents/discovery (project-local agents)Happy to open a separate issue for the above if the maintainers prefer tracking it.