Skip to content

Commit 43b1bab

Browse files
fix(settings): reject empty custom agent ids; align comment with consumers
Address Copilot review feedback on PR #123: - SaveCustomAcpAgent / SaveCustomDelegateAgent now bail out when DeriveCustomAgentId returns an empty id (whitespace-only or quote-only commands), so the UI cannot persist a bare `custom:` entry that would leave a blank, unusable custom agent selected. - Drop the stale `telemetry` mention from the consumer list comment in SaveCustomAcpAgent; telemetry sanitizes any non-built-in id to the literal `custom` and does not key on the prefix. - Rename `qwen` to `mybot` in CustomAgentAndPolicyTests.cpp to avoid check-spelling alerts without adding to the expect list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 06511fb commit 43b1bab

2 files changed

Lines changed: 26 additions & 20 deletions

File tree

src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,13 +552,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
552552
if (_GlobalSettings.IsCustomAgentPolicyLocked()) return;
553553
if (_customAcpCommand.empty()) return;
554554
const auto bareId = _DeriveId(_customAcpCommand);
555+
// Whitespace-only / quote-only commands derive to an empty id and
556+
// would otherwise be saved as a bare "custom:" entry, leaving the
557+
// UI with a blank, unusable custom agent. Reject before persisting.
558+
if (bareId.empty()) return;
555559
_GlobalSettings.AcpCustomCommand(_customAcpCommand);
556560

557561
// Custom agents always carry the "custom:" discriminator — every
558562
// downstream consumer (EffectiveAcpAgent policy gate, command-line
559-
// resolver, custom-edit/delete UI gates, telemetry) keys on this
560-
// prefix. Storing a bare id silently breaks all of them and makes
561-
// the page revert to the default agent on next load.
563+
// resolver, custom-edit/delete UI gates) keys on this prefix.
564+
// Storing a bare id silently breaks all of them and makes the page
565+
// revert to the default agent on next load.
562566
const bool isBuiltIn = _IsKnownAgent(bareId);
563567
const auto settingsId = winrt::hstring{ L"custom:" + std::wstring_view{ bareId } };
564568
const auto displayName = isBuiltIn
@@ -592,6 +596,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
592596
if (_GlobalSettings.IsCustomAgentPolicyLocked()) return;
593597
if (_customDelegateCommand.empty()) return;
594598
const auto bareId = _DeriveId(_customDelegateCommand);
599+
// See SaveCustomAcpAgent — reject empty derivations before persisting.
600+
if (bareId.empty()) return;
595601
_GlobalSettings.DelegateCustomCommand(_customDelegateCommand);
596602

597603
// See SaveCustomAcpAgent — always carry the "custom:" prefix.

src/cascadia/UnitTests_SettingsModel/CustomAgentAndPolicyTests.cpp

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,18 @@ namespace SettingsModelUnitTests
131131
// The whole point of PR #123: a custom agent must survive load
132132
// with its "custom:" prefix intact. If this regresses, the
133133
// settings page reverts to the default agent on next load.
134-
const auto settings = MakeSettings(R"("acpAgent": "custom:qwen", "acpCustomCommand": "qwen.cmd --acp")");
134+
const auto settings = MakeSettings(R"("acpAgent": "custom:mybot", "acpCustomCommand": "mybot.cmd --acp")");
135135
const auto& globals = settings->GlobalSettings();
136-
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:qwen" }, globals.AcpAgent());
137-
VERIFY_ARE_EQUAL(winrt::hstring{ L"qwen.cmd --acp" }, globals.AcpCustomCommand());
136+
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:mybot" }, globals.AcpAgent());
137+
VERIFY_ARE_EQUAL(winrt::hstring{ L"mybot.cmd --acp" }, globals.AcpCustomCommand());
138138
}
139139

140140
void CustomAgentAndPolicyTests::CustomDelegateAgentRoundtrips()
141141
{
142-
const auto settings = MakeSettings(R"("delegateAgent": "custom:qwen", "delegateCustomCommand": "qwen.cmd --acp")");
142+
const auto settings = MakeSettings(R"("delegateAgent": "custom:mybot", "delegateCustomCommand": "mybot.cmd --acp")");
143143
const auto& globals = settings->GlobalSettings();
144-
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:qwen" }, globals.DelegateAgent());
145-
VERIFY_ARE_EQUAL(winrt::hstring{ L"qwen.cmd --acp" }, globals.DelegateCustomCommand());
144+
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:mybot" }, globals.DelegateAgent());
145+
VERIFY_ARE_EQUAL(winrt::hstring{ L"mybot.cmd --acp" }, globals.DelegateCustomCommand());
146146
}
147147

148148
void CustomAgentAndPolicyTests::QuotedPathCustomCommandRoundtrips()
@@ -151,10 +151,10 @@ namespace SettingsModelUnitTests
151151
// are common for users on the Windows installer paths. Make sure
152152
// the parser preserves them verbatim.
153153
const auto settings = MakeSettings(
154-
R"("acpAgent": "custom:qwen", "acpCustomCommand": "\"C:\\Program Files\\qwen\\qwen.cmd\" --acp")");
154+
R"("acpAgent": "custom:mybot", "acpCustomCommand": "\"C:\\Program Files\\mybot\\mybot.cmd\" --acp")");
155155
const auto& globals = settings->GlobalSettings();
156-
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:qwen" }, globals.AcpAgent());
157-
VERIFY_ARE_EQUAL(winrt::hstring{ LR"("C:\Program Files\qwen\qwen.cmd" --acp)" },
156+
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:mybot" }, globals.AcpAgent());
157+
VERIFY_ARE_EQUAL(winrt::hstring{ LR"("C:\Program Files\mybot\mybot.cmd" --acp)" },
158158
globals.AcpCustomCommand());
159159
}
160160

@@ -215,14 +215,14 @@ namespace SettingsModelUnitTests
215215

216216
void CustomAgentAndPolicyTests::EffectiveAcpAgentCustomPassesWhenNoCustomPolicy()
217217
{
218-
const auto settings = MakeSettings(R"("acpAgent": "custom:qwen", "acpCustomCommand": "qwen.cmd")");
218+
const auto settings = MakeSettings(R"("acpAgent": "custom:mybot", "acpCustomCommand": "mybot.cmd")");
219219
SetPolicy(MakePolicy(/*allowedAgents*/ std::nullopt, AgentPolicy::PolicyState::NotConfigured));
220-
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:qwen" }, settings->GlobalSettings().EffectiveAcpAgent());
220+
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:mybot" }, settings->GlobalSettings().EffectiveAcpAgent());
221221
}
222222

223223
void CustomAgentAndPolicyTests::EffectiveAcpAgentCustomBlockedByCustomPolicy()
224224
{
225-
const auto settings = MakeSettings(R"("acpAgent": "custom:qwen", "acpCustomCommand": "qwen.cmd")");
225+
const auto settings = MakeSettings(R"("acpAgent": "custom:mybot", "acpCustomCommand": "mybot.cmd")");
226226
SetPolicy(MakePolicy(/*allowedAgents*/ std::nullopt, AgentPolicy::PolicyState::Blocked));
227227
VERIFY_ARE_EQUAL(winrt::hstring{}, settings->GlobalSettings().EffectiveAcpAgent());
228228
}
@@ -235,10 +235,10 @@ namespace SettingsModelUnitTests
235235
// Admin allowlist with only "gemini" — would block built-in
236236
// copilot. But a custom: agent passes through unchanged because
237237
// customAgents policy is NotConfigured / Allowed.
238-
const auto settings = MakeSettings(R"("acpAgent": "custom:qwen", "acpCustomCommand": "qwen.cmd")");
238+
const auto settings = MakeSettings(R"("acpAgent": "custom:mybot", "acpCustomCommand": "mybot.cmd")");
239239
SetPolicy(MakePolicy(std::set<std::wstring, AgentPolicy::CaseInsensitiveLess>{ L"gemini" },
240240
AgentPolicy::PolicyState::NotConfigured));
241-
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:qwen" }, settings->GlobalSettings().EffectiveAcpAgent());
241+
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:mybot" }, settings->GlobalSettings().EffectiveAcpAgent());
242242
}
243243

244244
// ── EffectiveDelegateAgent ──────────────────────────────────────────
@@ -266,17 +266,17 @@ namespace SettingsModelUnitTests
266266

267267
void CustomAgentAndPolicyTests::EffectiveDelegateAgentCustomBlockedByCustomPolicy()
268268
{
269-
const auto settings = MakeSettings(R"("delegateAgent": "custom:qwen", "delegateCustomCommand": "qwen.cmd")");
269+
const auto settings = MakeSettings(R"("delegateAgent": "custom:mybot", "delegateCustomCommand": "mybot.cmd")");
270270
SetPolicy(MakePolicy(/*allowedAgents*/ std::nullopt, AgentPolicy::PolicyState::Blocked));
271271
VERIFY_ARE_EQUAL(winrt::hstring{}, settings->GlobalSettings().EffectiveDelegateAgent());
272272
}
273273

274274
void CustomAgentAndPolicyTests::EffectiveDelegateAgentCustomIgnoresAllowedAgentsAllowlist()
275275
{
276-
const auto settings = MakeSettings(R"("delegateAgent": "custom:qwen", "delegateCustomCommand": "qwen.cmd")");
276+
const auto settings = MakeSettings(R"("delegateAgent": "custom:mybot", "delegateCustomCommand": "mybot.cmd")");
277277
SetPolicy(MakePolicy(std::set<std::wstring, AgentPolicy::CaseInsensitiveLess>{ L"gemini" },
278278
AgentPolicy::PolicyState::NotConfigured));
279-
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:qwen" }, settings->GlobalSettings().EffectiveDelegateAgent());
279+
VERIFY_ARE_EQUAL(winrt::hstring{ L"custom:mybot" }, settings->GlobalSettings().EffectiveDelegateAgent());
280280
}
281281

282282
// ── Lock-state ──────────────────────────────────────────────────────

0 commit comments

Comments
 (0)