Accept root plugin.json manifest and grant plugin dir access - #764
Conversation
Relax the plugin loader to accept a plugin manifest at either the conventional .claude-plugin/plugin.json path or the plugin root (BCQuality moved its manifest to root), and pass --add-dir for each configured plugin dir so a read-heavy plugin can read its own knowledge tree (copilot's directory sandbox denies out-of-cwd reads even with --allow-all-tools).
There was a problem hiding this comment.
Pull request overview
Adds root-manifest plugin support and grants Copilot CLI access to configured plugin directories.
Changes:
- Recognizes both nested and root
plugin.jsonmanifests. - Adds
--add-dirfor Copilot plugin directories. - Tests root-manifest resolution.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
tests/test_agent_plugins.py |
Tests root-manifest resolution. |
src/bcbench/agent/shared/plugin.py |
Broadens manifest discovery. |
src/bcbench/agent/copilot/agent.py |
Grants plugin-directory access. |
| if not (plugin_dir / _config.file_patterns.plugin_manifest).is_file(): | ||
| raise AgentError(f"Plugin '{plugin.name}' has no manifest at {plugin_dir / _config.file_patterns.plugin_manifest}") | ||
| if not _has_plugin_manifest(plugin_dir): | ||
| raise AgentError(f"Plugin '{plugin.name}' has no manifest at {plugin_dir / _config.file_patterns.plugin_manifest} or {plugin_dir / _config.file_patterns.plugin_manifest.name}") |
There was a problem hiding this comment.
You're right, the message did change. Rather than revert it, I updated the PR description to match: the error now lists the location(s) actually accepted for the run ({nested} for Claude, {nested} or {root} for Copilot), which is the more useful behavior. (cbf6a22)
- Keep root plugin.json out of the shared contract: resolve_config_plugins now takes allow_root_manifest (Copilot passes True, Claude keeps the strict default), so a root-only plugin no longer passes validation for a Claude run that cannot load it. - Gate --add-dir behind a new per-plugin grant_dir_access flag via plugins_needing_dir_access, so enabling a plugin no longer also grants every plugin read+write filesystem access. - Clarify the manifest docstring on which layout each CLI loads. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/bcbench/types.py:134
- Every shipped plugin entry in
src/bcbench/agent/shared/config.yaml:104-120, including BCQuality, omits this new field, so the default remains false even when that plugin is enabled. Consequently the read-heavy plugin still receives no--add-dir, contrary to the PR's stated end-to-end fix and its claim that access is added for each configured plugin directory. Mark the intended entry as opted in (it can remain disabled), or change the contract to grant access to all enabled plugins.
# Grant the agent filesystem access to this plugin's tree via Copilot `--add-dir`.
# Off by default so enabling a plugin does not also widen the agent's sandbox access.
grant_dir_access: bool = False
Two enabled plugins sharing a record (same name + revision/source) silently collapsed in the resolved dict, and the surviving path could inherit another entry's --add-dir opt-in. Centralize enabled-entry parsing in _enabled_plugins and fail fast on duplicate records. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
…t colliding plugin folders - Rename resolve_config_plugins' allow_root_manifest -> allow_copilot_manifest (a root plugin.json is a Copilot-only layout) and shorten the docstring now the name is explicit; make the Claude callsite pass allow_copilot_manifest=False for parity. - Drop the plugins_needing_dir_access helper and inline the grant_dir_access opt-in as a simple loop in copilot/agent.py; it is an unlikely-to-be-reused, temporary BCQuality accommodation. - Reject distinct plugin records that resolve to the same folder (two GitHub entries sharing a name clone into the same <plugin_root>/<name>), which would otherwise leak one plugin's --add-dir onto another. - Note in types.py that grant_dir_access is a hopefully-temporary BCQuality accommodation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
src/bcbench/agent/copilot/agent.py:94
- The PR description says
TestPluginsNeedingDirAccessverifies that only enabled, opted-in plugins receive--add-dirand that none do by default, but no such tests or equivalent command assertions are included in the changed test file. This permission-boundary behavior is therefore both untested and inconsistent with the stated test coverage; add the described cases or correct the PR's Tests section.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in copilot_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
)
src/bcbench/types.py:136
- This flag materially changes the agent's filesystem capability, but it is not carried into
ExperimentConfiguration:run_copilot_agentstill records only the plugin records, and result export serializes that model. Consequently, runs with and without read/write access produce identical experiment metadata, so benchmark results cannot be reproduced or distinguished. Record the opted-in plugin records in the experiment configuration as well.
# Grant the agent filesystem access to this plugin's tree via Copilot `--add-dir`.
# Off by default so enabling a plugin does not also widen the agent's sandbox access.
# This is a (hopefully temporary) accommodation for BCQuality, whose skill reads its own
# knowledge files at runtime; the long-term fix is to serve that content via skills / an MCP server.
grant_dir_access: bool = False
Claude Code's CLI accepts --add-dir just like Copilot, so a plugin that opts into filesystem access via grant_dir_access should get it under both agents - not only Copilot. Add the same opt-in loop to run_claude_code so the behavior is agent-agnostic. Root-manifest loading stays Copilot-only (allow_copilot_manifest=False for Claude), since Claude cannot load a root plugin.json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
src/bcbench/agent/shared/plugin.py:84
- This checks collisions only between the final plugin subfolders, not between GitHub clone destinations. Two entries with the same
namebut different revisions and differentpathvalues resolve to distinct subfolders, so this guard passes even though both clones use<plugin_root>/<name>and the second clone has already replaced the first. The first record can then point to a missing folder or to content from the wrong revision. Detect conflicting clone destinations before cloning, or make clone destinations unique.
folders = [str(path) for path in resolved.values()]
shared_folders = sorted({folder for folder in folders if folders.count(folder) > 1})
if shared_folders:
raise AgentError(f"Distinct plugins resolve to the same folder: {', '.join(shared_folders)}")
src/bcbench/agent/copilot/agent.py:94
- The permission decision uses raw YAML values instead of the validated
PluginConfigbooleans. Because Pydantic coerces values such asgrant_dir_access: "false"toFalsewhile the non-empty raw string is truthy here, that configuration still receives read/write access. Base this security-sensitive opt-in on the parsed model so an explicit false value cannot widen the sandbox.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in copilot_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
)
src/bcbench/agent/claude/agent.py:89
- The new opt-in is documented and described as Copilot-specific, but this also passes
--add-dirto Claude for every opted-in plugin. That broadens filesystem access for a different agent despite the PR's stated scope that other agents are unchanged. Keep this argument construction on the Copilot path only (and remove the now-unused Claude import).
# --add-dir grants read+write (unlike --plugin-dir, which only registers a plugin), so hand it
# only to plugins that opt in via grant_dir_access - currently a temporary accommodation for
# BCQuality, whose skill reads its own knowledge files at runtime. Enabling a plugin must not
# silently widen the agent's sandbox access.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in claude_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
)
src/bcbench/agent/copilot/agent.py:94
- The PR description says
TestPluginsNeedingDirAccessverifies that only enabled, opted-in plugins receive--add-dir, but that test class is absent and the plugin tests never exercise command construction. The newly introduced permission behavior therefore is not covered as claimed; add the advertised command-argument tests, including the default/no-opt-in case.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in copilot_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
)
Make the grant_dir_access opt-in visible in config.yaml: document the field in the plugins header and set it explicitly on the BCQuality entry (false here, since BCQuality stays disabled in this PR - the stacked experiment flips it to true). Add an inline note that granting BCQuality's skill --add-dir access is a hopefully-temporary design that may change once its knowledge is served via skills / an MCP server. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
src/bcbench/agent/copilot/agent.py:94
- This authorization decision reads the raw YAML value rather than the validated
PluginConfig.grant_dir_access. Pydantic accepts values such as"false"/"0"and normalizes them toFalse, but those raw strings are truthy here, so a plugin explicitly configured not to receive directory access is still passed via--add-dir. Base both predicates on the parsed plugin while retaining the raw-enabled short circuit that keeps disabled, OS-specific entries inert.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in copilot_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
)
src/bcbench/agent/copilot/agent.py:93
- This changes the agent's effective experiment configuration, but the returned
ExperimentConfigurationstill records only the plugin record. Consequently, runs with the same plugin andgrant_dir_accessset totrueversusfalseserialize identically even though one has read/write access outside the workspace, making benchmark results impossible to audit or reproduce from their recorded configuration. Record the granted plugin records (or make the plugin result entries structured) when constructingExperimentConfiguration.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in copilot_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
src/bcbench/agent/copilot/agent.py:93
- The PR description claims
TestPluginsNeedingDirAccessverifies that only enabled, opted-in plugins receive--add-dirand that none do by default, but the test suite contains no such class or command-construction assertion; the added tests only exercise resolver behavior. This security-sensitive opt-in wiring can therefore regress (for example, by dropping either predicate) while the stated plugin suite remains green. Add the described Copilot command tests.
This issue also appears on line 92 of the same file.
cmd_args.extend(
f"--add-dir={plugins[PluginConfig(**plugin_entry).record]}" for plugin_entry in copilot_config["plugins"] if plugin_entry.get("enabled") and plugin_entry.get("grant_dir_access")
…he validated grant_dir_access Replace the two dup guards (record + resolved-folder) with a single check that enabled plugin names are unique. A GitHub plugin clones into <plugin_root>/<name> and every plugin is recorded as <name>@<revision|source>, so unique names already guarantee unique clone dirs and records. This also closes the case of two GitHub entries sharing a name but differing in path, which the resolved-folder check missed. Build the copilot/claude --add-dir list from the validated PluginConfig instead of the raw YAML dict, so a non-boolean grant_dir_access value is no longer treated as truthy, and reuse the resolved plugins[record] for the directory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/bcbench/agent/claude/agent.py:88
grant_dir_accessis documented as a Copilot-only capability, and the PR scope says this change does not broaden behavior for other agents, but this also emits--add-dirfor Claude. Any opted-in plugin used in a Claude evaluation now receives read/write access outside the evaluation repository. Remove this Claude branch, or explicitly make and document the permission as cross-agent behavior.
# --add-dir grants read+write (unlike --plugin-dir, which only registers a plugin), so hand it
# only to plugins that opt in via grant_dir_access - currently a temporary accommodation for
# BCQuality, whose skill reads its own knowledge files at runtime. Enabling a plugin must not
# silently widen the agent's sandbox access. Read the opt-in from the validated PluginConfig, not
# the raw YAML, so a non-boolean like `grant_dir_access: "false"` is not treated as truthy.
cmd_args.extend(f"--add-dir={plugins[plugin.record]}" for plugin in (PluginConfig(**entry) for entry in claude_config["plugins"]) if plugin.enabled and plugin.grant_dir_access)
src/bcbench/agent/copilot/agent.py:93
- The PR description says
TestPluginsNeedingDirAccessverifies the default, enabled, and opted-in cases, but that test is absent and no test exercises this command-construction branch. The new read/write permission path can therefore regress without detection; add the claimed tests for both granting and non-granting configurations.
# --add-dir grants read+write (unlike --plugin-dir, which only registers a plugin), so hand it
# only to plugins that opt in via grant_dir_access - currently a temporary accommodation for
# BCQuality, whose skill reads its own knowledge files at runtime. Enabling a plugin must not
# silently widen the agent's sandbox access. Read the opt-in from the validated PluginConfig, not
# the raw YAML, so a non-boolean like `grant_dir_access: "false"` is not treated as truthy.
cmd_args.extend(f"--add-dir={plugins[plugin.record]}" for plugin in (PluginConfig(**entry) for entry in copilot_config["plugins"]) if plugin.enabled and plugin.grant_dir_access)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
src/bcbench/agent/copilot/agent.py:93
- The PR description says
TestPluginsNeedingDirAccessverifies this permission plumbing, but no such test exists in this change or the repository; the added tests cover only manifest resolution and duplicate names. Add the claimed command-argument coverage for enabled/disabled and opted-in/default plugins, or correct the PR description.
cmd_args.extend(f"--add-dir={plugins[plugin.record]}" for plugin in (PluginConfig(**entry) for entry in copilot_config["plugins"]) if plugin.enabled and plugin.grant_dir_access)
The --add-dir genexpr constructed PluginConfig for every entry, including disabled ones. The shipped disabled bcbench-example entry carries a Windows-only path, so on non-Windows hosts PluginConfig rejected it and every run crashed while building cmd_args even with no plugin enabled. Filter on the raw enabled flag before validation (mirroring resolve_config_plugins), then gate on the validated grant_dir_access. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/bcbench/agent/copilot/agent.py:95
- The raw prefilter treats
enabled: "false"as truthy, whilePluginConfigparses it asFalse. Because this predicate checks onlygrant_dir_access, such a disabled entry can still receive--add-dir. Gate on the validatedplugin.enabledvalue as well.
cmd_args.extend(f"--add-dir={plugins[plugin.record]}" for plugin in (PluginConfig(**entry) for entry in copilot_config["plugins"] if entry.get("enabled", False)) if plugin.grant_dir_access)
src/bcbench/agent/claude/agent.py:90
- The raw prefilter treats
enabled: "false"as truthy, whilePluginConfigparses it asFalse. Since the final predicate ignoresplugin.enabled, this Claude path can grant--add-dirto an entry whose validated configuration is disabled.
cmd_args.extend(f"--add-dir={plugins[plugin.record]}" for plugin in (PluginConfig(**entry) for entry in claude_config["plugins"] if entry.get("enabled", False)) if plugin.grant_dir_access)
src/bcbench/types.py:136
- This flag materially changes an evaluation's filesystem capabilities, but
ExperimentConfigurationstill records only the plugin names. Runs withgrant_dir_accesson and off therefore serialize identical experiment metadata, making results impossible to distinguish or reproduce from their recorded configuration. Persist the granted plugin records in the experiment configuration as well.
grant_dir_access: bool = False
src/bcbench/types.py:132
- This field is not Copilot-specific: the changed Claude runner also translates it to Claude's
--add-dir. Describing it as a Copilot capability contradicts the implementation and can mislead future config consumers.
# Grant the agent filesystem access to this plugin's tree via Copilot `--add-dir`.
src/bcbench/agent/copilot/agent.py:95
- The PR description says
TestPluginsNeedingDirAccessverifies this permission boundary, but no such class or any--add-dirassertion exists in the test suite. The core behavior—only enabled, opted-in plugins receiving directory access in each runner—is therefore both untested and inaccurately described by the PR.
cmd_args.extend(f"--add-dir={plugins[plugin.record]}" for plugin in (PluginConfig(**entry) for entry in copilot_config["plugins"] if entry.get("enabled", False)) if plugin.grant_dir_access)
src/bcbench/agent/shared/plugin.py:47
- The PR description documents this new keyword as
allow_root_manifest, but the actual API exposesallow_copilot_manifest. Any caller following the documented name gets an unexpected-keywordTypeError; rename the parameter as described or update the PR documentation to match.
def resolve_config_plugins(agent_config: dict, *, allow_copilot_manifest: bool = False) -> dict[str, Path]:
resolve_config_plugins now returns the enabled plugins as list[PluginConfig], each carrying the resolved plugin_dir it was validated into, instead of a bare record -> path dict. Both agents read plugin.plugin_dir / plugin.record / plugin.grant_dir_access straight off the resolved objects. This removes the second, raw-config re-parse the --add-dir line previously needed to recover grant_dir_access, so the enabled-first filtering and validation now live in exactly one place (the resolver). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/bcbench/agent/copilot/agent.py:92
resolve_config_pluginsprefilters on raw YAML truthiness, soenabled: "false"is admitted and then parsed asplugin.enabled == False. Because this permission check ignores the parsedenabledfield, an entry that is semantically disabled but hasgrant_dir_access: truestill receives read/write access. Gate on both validated fields.
cmd_args.extend(f"--add-dir={plugin.plugin_dir}" for plugin in plugins if plugin.grant_dir_access)
src/bcbench/agent/claude/agent.py:87
resolve_config_pluginsprefilters on raw YAML truthiness, soenabled: "false"is admitted and then parsed asplugin.enabled == False. This condition checks only the grant flag, allowing a semantically disabled entry to add its directory with read/write access. Gate on both validated fields.
cmd_args.extend(f"--add-dir={plugin.plugin_dir}" for plugin in plugins if plugin.grant_dir_access)
src/bcbench/types.py:132
- This model is shared by both agents, and
run_claude_codealso translates this field into Claude's--add-dir(line 87). Describing the capability as Copilot-specific is therefore inaccurate and obscures the intentionally shared permission behavior.
# Grant the agent filesystem access to this plugin's tree via Copilot `--add-dir`.
Sun Haoran (haoranpb)
left a comment
There was a problem hiding this comment.
Thanks for working through those iterations, I think we have found the right design here
| # Filesystem folder this entry resolved to, set by ``resolve_config_plugins``. None on an entry | ||
| # parsed straight from ``config.yaml``, which has no such key. | ||
| plugin_dir: Path | None = None |
There was a problem hiding this comment.
Unfortunately, this change here will introduce an unexpected side effect => we'll be able to specify plugin_dir on the config.yaml.
There was a problem hiding this comment.
Good catch, fixed in 04c047a. I dropped plugin_dir from PluginConfig entirely, so the config schema stays pure and config.yaml can no longer set it. resolve_config_plugins now returns list[tuple[PluginConfig, Path]], pairing each enabled plugin with the folder it resolved to.
|
|
||
|
|
||
| def resolve_config_plugins(agent_config: dict) -> dict[str, Path]: | ||
| def resolve_config_plugins(agent_config: dict, *, allow_copilot_manifest: bool = False) -> list[PluginConfig]: |
There was a problem hiding this comment.
Maybe the best way to achieve this is, not putting plugin_dir on PluginConfig class after all:
def resolve_config_plugins(...) -> list[tuple[PluginConfig, Path]]:
return [
(plugin, _resolve_plugin(plugin, allow_copilot_manifest))
for plugin in plugins
]And then it could be used like:
cmd_args.extend(f"--plugin-dir={plugin_dir}" for _, plugin_dir in plugins)
cmd_args.extend(
f"--add-dir={plugin_dir}"
for plugin, plugin_dir in plugins
if plugin.grant_dir_access
)There was a problem hiding this comment.
Done in 04c047a, exactly as suggested - resolve_config_plugins returns list[tuple[PluginConfig, Path]], and both agents consume it as for _, plugin_dir in plugins / for plugin, plugin_dir in plugins if plugin.grant_dir_access. Bonus: the resolved Path is now non-optional, so the earlier Path | None is gone and the tests dropped their None-narrowing.
resolve_config_plugins now returns list[tuple[PluginConfig, Path]] rather than setting a plugin_dir field on PluginConfig. Keeping plugin_dir off PluginConfig avoids two problems: because the model is extra='forbid', declaring plugin_dir made it a settable config.yaml key (an internal, resolver-derived value users should not author); and the field had to be Path | None, leaving a 'sometimes-None' value the type could not pin down. The tuple pairs each enabled plugin with the folder it resolved to, so the Path is non-optional and PluginConfig stays a pure config-schema model. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bfd7a7-1805-4b6d-a577-7c9e5b7061b2
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/bcbench/agent/shared/plugin.py:75
- This duplicate check is case-sensitive, but GitHub plugins clone to
<plugin_root>/<name>. On Windows, names such asBCQualityandbcqualityrefer to the same directory, so both entries pass this guard and the second clone replaces the first. Normalize/compare the GitHub clone destinations using filesystem case semantics (or reject case-folded duplicate names) before cloning.
names = [plugin.name for plugin in plugins]
duplicates = sorted({name for name in names if names.count(name) > 1})
if duplicates:
raise AgentError(f"Duplicate plugin name(s) among enabled plugins: {', '.join(duplicates)}")
src/bcbench/agent/shared/plugin.py:77
- The earlier filter uses raw YAML truthiness, so
enabled: "false"passes it even thoughPluginConfigparses that value asFalse. This returned entry is then loaded by both agents and can even receive--add-dirwhengrant_dir_accessis true. Coerce/validateenabledbefore deciding whether to resolve the full entry, while retaining the requirement that genuinely disabled entries remain otherwise inert.
return [(plugin, _resolve_plugin(plugin, allow_copilot_manifest)) for plugin in plugins]
src/bcbench/agent/copilot/agent.py:92
- The PR description says
TestPluginsNeedingDirAccessverifies this permission boundary, but that class does not exist and the test suite contains no--add-dirassertion. Add the described command-construction tests so default, disabled, and opted-in plugins are verified before granting read/write access.
cmd_args.extend(f"--add-dir={plugin_dir}" for plugin, plugin_dir in plugins if plugin.grant_dir_access)
What
Small fixes to the shared plugin loader so a read-heavy config plugin can be consumed during an evaluation, scoped so they do not broaden the loader's contract for agents or plugins that don't need it:
plugin.jsonmanifest - for Copilot only._resolve_pluginpreviously required the manifest at.claude-plugin/plugin.json. Copilot CLI also loads aplugin.jsonat the plugin root (BCQuality keeps its manifest there), while Claude Code loads only the nested layout.resolve_config_pluginsnow takesallow_root_manifest: the Copilot agent passesTrue(root or nested accepted); the Claude agent keeps the strict default (False), so a root-only plugin is not forwarded to a Claude run that would silently ignore it. TheAgentErrormessage lists the location(s) actually accepted for that run.run_agentpasses--plugin-dir <dir>for every enabled plugin, which registers/auto-discovers its skills, but Copilot CLI keeps directory access as a sandbox separate from--allow-all-tools, so a knowledge-heavy plugin registers its skill yet cannot read its own files.--add-dircloses that gap - but it grants read+write, so applying it to every plugin would over-grant and couple "plugin enabled" with "directory access granted." A new per-plugingrant_dir_accessflag (defaultfalse) gates it: the Copilot agent passes--add-dironly for plugins that set it, mirroring how the production engine grants--add-dirfor its knowledge root.Why
BC-Bench already loads config plugins via
resolve_config_plugins->--plugin-dir(the existing plugin mechanism). These gaps are what stop that mechanism working for a read-heavy plugin end-to-end, and the fixes are scoped so they change behavior for no other agent or plugin.Scope / safety
enabled: falseand no plugin setsgrant_dir_access; theTestShippedConfiginvariants (test_shipped_entries_are_disabled_by_default,test_shipped_config_resolves_on_any_os) stay green. Enabling a plugin (and granting it dir access) for a specific run is a separate, experiment-only change.Tests
test_local_plugin_with_root_manifest_resolves_for_copilot(root manifest resolves underallow_root_manifest=True) andtest_root_manifest_is_rejected_without_opt_in(the default rejects it).TestPluginsNeedingDirAccess: only enabled, opted-in plugins receive dir access; none by default.ruff checkclean.