Forge
csharpdeeb25a2
1using System.Collections.Frozen;
2using System.Text.Json;
3using CommunityToolkit.Mvvm.ComponentModel;
4
5namespace CascadeIDE.Features.WebAiPortal.Application;
6
7/// <summary>Мост веб-портала ADR 0108: JSON с <c>command_id</c> + <c>args</c> → whitelist → <see cref="IIdeMcpActions"/>.</summary>
8public sealed class WebAiPortalCommandBridge : ObservableObject
9{
10 private readonly IIdeMcpActions _actions;
11
12 private bool _bridgeConsented;
13 private bool _bridgeArmed;
14 private bool _allowWritesForSession;
15
16 /// <summary>Согласие с политикой моста (ADR 0108 §2.6).</summary>
17 public bool BridgeConsented
18 {
19 get => _bridgeConsented;
20 set => SetProperty(ref _bridgeConsented, value);
21 }
22
23 /// <summary>Включить приём сообщений от страницы (<c>invokeCSharpAction</c>).</summary>
24 public bool BridgeArmed
25 {
26 get => _bridgeArmed;
27 set => SetProperty(ref _bridgeArmed, value);
28 }
29
30 /// <summary>Разрешить write без модалки на каждый вызов (ADR 0108 §2.3 п.8).</summary>
31 public bool AllowWritesForSession
32 {
33 get => _allowWritesForSession;
34 set => SetProperty(ref _allowWritesForSession, value);
35 }
36
37 /// <summary>PoC-allowlist ADR 0108 §2.2 (канонические <c>IdeCommands</c>).</summary>
38 internal static readonly FrozenSet<string> Whitelist = FrozenSet.ToFrozenSet(
39 new[]
40 {
41 IdeCommands.GetEditorContentRange,
42 IdeCommands.GetEditorState,
43 IdeCommands.CodebaseIndexStatus,
44 IdeCommands.CodebaseIndexSearch,
45 IdeCommands.CodebaseIndexExplain,
46 IdeCommands.GetCurrentFileDiagnostics,
47 },
48 StringComparer.Ordinal);
49
50 /// <summary>Команды, считающиеся write и требующие подтверждения (пока пусто — весь whitelist чтение).</summary>
51 internal static readonly FrozenSet<string> WriteCommands = FrozenSet.ToFrozenSet(
52 Array.Empty<string>(),
53 StringComparer.Ordinal);
54
55 public WebAiPortalCommandBridge(IIdeMcpActions actions) => _actions = actions;
56
57 public void RevokeBridge()
58 {
59 BridgeArmed = false;
60 BridgeConsented = false;
61 AllowWritesForSession = false;
62 }
63
64 /// <summary>Тело сообщения от <c>invokeCSharpAction</c>: JSON одной строкой или объект с полями MCP execute_command.</summary>
65 public async Task<string> ExecuteFromWebJsonAsync(string body, CancellationToken cancellationToken = default)
66 {
67 if (string.IsNullOrWhiteSpace(body))
68 return "Empty body";
69 if (!BridgeConsented || !BridgeArmed)
70 return "Web AI bridge is not armed (consent and enable required in IDE).";
71
72 Dictionary<string, JsonElement>? dict;
73 try
74 {
75 dict = ParseToArgDictionary(body);
76 }
77 catch (JsonException ex)
78 {
79 return "Invalid JSON: " + ex.Message;
80 }
81
82 if (dict is null)
83 return "Invalid JSON: expected object.";
84
85 var merged = IdeExecuteCommandArgs.MergeNestedArgs(dict);
86 var commandId = merged is not null && merged.TryGetValue("command_id", out var cid) ? cid.GetString() : null;
87 if (string.IsNullOrEmpty(commandId))
88 return "Missing command_id";
89
90 if (!Whitelist.Contains(commandId))
91 return $"Command not allowed by web portal whitelist: {commandId}";
92
93 if (WriteCommands.Contains(commandId))
94 {
95 if (!AllowWritesForSession)
96 {
97 var ok = await _actions
98 .RequestConfirmationAsync(
99 $"Веб-портал запрашивает команду (изменение): {commandId}. Разрешить один раз?",
100 cancellationToken)
101 .ConfigureAwait(false);
102 if (!string.Equals(ok, ConfirmationResponses.Ok, StringComparison.OrdinalIgnoreCase))
103 return "Denied by user";
104 }
105 }
106
107 return await _actions.ExecuteCommandAsync(commandId, merged, cancellationToken).ConfigureAwait(false);
108 }
109
110 private static Dictionary<string, JsonElement>? ParseToArgDictionary(string body)
111 {
112 using var doc = JsonDocument.Parse(body);
113 if (doc.RootElement.ValueKind != JsonValueKind.Object)
114 return null;
115 var dict = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
116 foreach (var p in doc.RootElement.EnumerateObject())
117 dict[p.Name] = p.Value.Clone();
118 return dict;
119 }
120}
121
View only · write via MCP/CIDE