Expose scoped plugin configuration indexes

Let project- or agent-scoped plugins open the shared settings surface even without a custom form. Tool Access can now reach existing scoped configurations while inert Save and Reset actions remain hidden.

Alessandro committed Aug 11, 2026 at 19:03 UTC b5006d571cd755e9833794ced6506bbf33cec701
8 files changed +60 -13
plugins/_tool_access/AGENTS.md
+3 -4
@@ -12,10 +12,9 @@
12
13 ## Local Contracts
14
15 -- This plugin has no independent settings UI; the Agent Editor writes sparse
16 - profile `config.json` files, projects may own project or project-profile
17 - configs through the standard plugin scope paths, and the runtime remains
18 - authoritative.
15 +- This plugin has no independent settings form; the generic plugin settings
16 + shell exposes its scoped configuration index, while Agent Editor writes
17 + sparse profile `config.json` files and the runtime remains authoritative.
18 - Custom configuration stores independent `default` and `mcp_default`
19 fallbacks; explicit canonical IDs remain shared in `allowed` and `blocked`.
20 - Required final-response capability is never disabled.
plugins/_tool_access/README.md
+2
@@ -5,6 +5,8 @@ Policies use the standard plugin precedence: active project profile, active
5 project, user profile, bundled/plugin profile, then the default. Sparse project
6 policy lives under `.a0proj/plugins/_tool_access/config.json`; profile policy
7 lives under `usr/agents/<profile>/plugins/_tool_access/config.json`.
8 +The Plugins screen exposes these scoped files through its existing
9 +configuration list; policy choices remain owned by Agent Editor.
10
11 One resolver filters textual prompts and provider-native schemas, rejects local
12 and MCP execution, and keeps delegated agents bound to their own effective scope.
tests/test_plugin_activation_ui.py
+23
@@ -124,6 +124,29 @@ def test_config_scope_activation_toggle_saves_immediately_for_selected_scope():
124 assert "action: \"toggle_plugin\"" in toggle_store
125
126
127 +def test_scoped_plugin_without_settings_form_exposes_configuration_index():
128 + list_html = (
129 + PROJECT_ROOT / "webui/components/plugins/list/plugin-list.html"
130 + ).read_text(encoding="utf-8")
131 + list_store = (
132 + PROJECT_ROOT / "webui/components/plugins/list/pluginListStore.js"
133 + ).read_text(encoding="utf-8")
134 + settings_html = (
135 + PROJECT_ROOT / "webui/components/plugins/plugin-settings.html"
136 + ).read_text(encoding="utf-8")
137 + settings_store = (
138 + PROJECT_ROOT / "webui/components/plugins/plugin-settings-store.js"
139 + ).read_text(encoding="utf-8")
140 +
141 + assert "canOpenPluginConfig(plugin)" in list_html
142 + assert "plugin?.per_project_config" in list_store
143 + assert "plugin?.per_agent_config" in list_store
144 + assert "!pluginMeta.per_project_config" in settings_store
145 + assert "!pluginMeta.per_agent_config" in settings_store
146 + assert "Scoped configurations" in settings_html
147 + assert "context.pluginMeta?.has_config_screen" in settings_html
148 +
149 +
150 def test_toggle_plugin_writes_project_scope_file_immediately(tmp_path, monkeypatch):
151 monkeypatch.setattr(files, "_base_dir", str(tmp_path))
152 monkeypatch.setattr(plugins, "after_plugin_change", lambda *_args, **_kwargs: None)
webui/components/plugins/AGENTS.md
+3
@@ -17,6 +17,9 @@
17 - Preserve global and scoped toggle semantics using `.toggle-1` and `.toggle-0`.
18 - Use notification helpers for plugin UI feedback.
19 - Label reset actions `Reset to default` and resolve notification globals when the action runs so late WebUI initialization cannot abort the reset lifecycle.
20 +- Scoped plugins without a custom settings form still expose the standard scope
21 + selectors and existing-configuration list; do not show inert Save or Reset
22 + actions for them.
23
24 ## Work Guidance
25
webui/components/plugins/list/plugin-list.html
+1 -1
@@ -105,7 +105,7 @@
105 <x-icon class="icon" name="dashboard"></x-icon> Open
106 </button>
107 </template>
108 - <template x-if="plugin.has_config_screen">
108 + <template x-if="$store.pluginListStore.canOpenPluginConfig(plugin)">
109 <button type="button"
110 class="button"
111 title="Config"
webui/components/plugins/list/pluginListStore.js
+12 -3
@@ -93,14 +93,23 @@ const model = {
93 pluginExecuteStore.open(plugin);
94 },
95
96 + canOpenPluginConfig(plugin) {
97 + return !!(
98 + plugin?.has_config_screen ||
99 + plugin?.per_project_config ||
100 + plugin?.per_agent_config
101 + );
102 + },
103 +
104 async openPluginConfig(pluginOrName) {
105 const pluginName =
106 typeof pluginOrName === "string" ? pluginOrName : pluginOrName?.name;
107 if (!pluginName) return;
108
101 - // If it's an object, we can check has_config_screen.
102 - // If it's a name, we just try to open it and let pluginSettingsStore handle errors.
103 - if (typeof pluginOrName === "object" && !pluginOrName.has_config_screen)
109 + if (
110 + typeof pluginOrName === "object" &&
111 + !this.canOpenPluginConfig(pluginOrName)
112 + )
113 return;
114
115 try {
webui/components/plugins/plugin-settings-store.js
+7 -3
@@ -278,8 +278,12 @@ const model = {
278 if (!pluginMeta) {
279 throw new Error(`Plugin "${pluginName}" not found.`);
280 }
281 - if (!pluginMeta.has_config_screen) {
282 - throw new Error(`Plugin "${pluginName}" has no config screen.`);
281 + if (
282 + !pluginMeta.has_config_screen &&
283 + !pluginMeta.per_project_config &&
284 + !pluginMeta.per_agent_config
285 + ) {
286 + throw new Error(`Plugin "${pluginName}" has no configurable scope.`);
287 }
288
289 await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
@@ -432,7 +436,7 @@ const model = {
436
437 // Reactive URL for the plugin's settings component (used with x-html injection)
438 get settingsComponentHtml() {
435 - if (!this.pluginName) return "";
439 + if (!this.pluginName || !this.pluginMeta?.has_config_screen) return "";
440 return `<x-component path="/plugins/${this.pluginName}/webui/config.html"></x-component>`;
441 },
442 };
webui/components/plugins/plugin-settings.html
+9 -2
@@ -117,6 +117,13 @@
117 class="plugin-settings-body"
118 x-html="context.settingsComponentHtml">
119 </div>
120 + <div x-show="!context.isLoading && !context.pluginMeta?.has_config_screen"
121 + class="plugin-settings-body">
122 + <div class="section-title">Scoped configurations</div>
123 + <div class="section-description">
124 + Use the scope controls and list button above to review or remove existing configurations.
125 + </div>
126 + </div>
127
128 </div>
129
@@ -136,7 +143,7 @@
143
144 <div class="plugin-settings-footer-actions">
145 <button class="btn"
139 - x-show="!context.hideSettingsActions"
146 + x-show="!context.hideSettingsActions && context.pluginMeta?.has_config_screen"
147 @click="context.resetToDefault()"
148 :disabled="context?.isSaving || context?.isLoading">
149 Reset to default
@@ -148,7 +155,7 @@
155 <span x-text="context.wizardFooter?.nextLabel?.() || 'Next'"></span>
156 </button>
157 <button class="btn btn-ok"
151 - x-show="!context.hideSettingsActions && (!context.wizardFooter || context.wizardFooter?.showSave?.())"
158 + x-show="!context.hideSettingsActions && context.pluginMeta?.has_config_screen && (!context.wizardFooter || context.wizardFooter?.showSave?.())"
159 @click="context.save()"
160 :disabled="context?.isSaving || context?.isLoading">
161 Save