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