@atelierai/uco 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -8
  3. package/bin/server.mjs +1 -1
  4. package/bin/stub-plugin.mjs +3 -3
  5. package/dist/codegen/coerce.js +1 -1
  6. package/dist/codegen/coerce.js.map +1 -1
  7. package/dist/codegen/emit.js +2 -2
  8. package/dist/codegen/emit.js.map +1 -1
  9. package/dist/codegen/refs.js +1 -1
  10. package/dist/codegen/refs.js.map +1 -1
  11. package/dist/codegen/types.js +1 -1
  12. package/dist/codegen/types.js.map +1 -1
  13. package/dist/commands/call.js +1 -1
  14. package/dist/commands/call.js.map +1 -1
  15. package/dist/commands/devops/configure.js +1 -1
  16. package/dist/commands/devops/configure.js.map +1 -1
  17. package/dist/commands/devops/index.js +0 -4
  18. package/dist/commands/devops/index.js.map +1 -1
  19. package/dist/commands/devops/install-plugin.js +1 -1
  20. package/dist/commands/devops/install-plugin.js.map +1 -1
  21. package/dist/commands/devops/open.js +7 -7
  22. package/dist/commands/devops/open.js.map +1 -1
  23. package/dist/commands/devops/status.js +3 -3
  24. package/dist/commands/devops/status.js.map +1 -1
  25. package/dist/commands/exec.js +1 -1
  26. package/dist/commands/exec.js.map +1 -1
  27. package/dist/commands/install.js +2 -2
  28. package/dist/commands/install.js.map +1 -1
  29. package/dist/commands/prompt.js +2 -2
  30. package/dist/commands/prompt.js.map +1 -1
  31. package/dist/commands/resource.js +2 -2
  32. package/dist/commands/resource.js.map +1 -1
  33. package/dist/commands/update.js +1 -26
  34. package/dist/commands/update.js.map +1 -1
  35. package/dist/config/port.js +2 -2
  36. package/dist/config/port.js.map +1 -1
  37. package/dist/config/resolve.d.ts +1 -2
  38. package/dist/config/resolve.js +0 -12
  39. package/dist/config/resolve.js.map +1 -1
  40. package/dist/devops/lib/configure.d.ts +1 -1
  41. package/dist/devops/lib/configure.js +2 -2
  42. package/dist/devops/lib/configure.js.map +1 -1
  43. package/dist/devops/lib/install-plugin.d.ts +1 -1
  44. package/dist/devops/lib/install-plugin.js +1 -1
  45. package/dist/devops/lib/install-plugin.js.map +1 -1
  46. package/dist/devops/lib/install.d.ts +2 -2
  47. package/dist/devops/lib/install.js +5 -5
  48. package/dist/devops/lib/install.js.map +1 -1
  49. package/dist/devops/lib/open.js +11 -32
  50. package/dist/devops/lib/open.js.map +1 -1
  51. package/dist/devops/lib/remove-plugin.d.ts +1 -1
  52. package/dist/devops/lib/remove-plugin.js +3 -3
  53. package/dist/devops/lib/remove-plugin.js.map +1 -1
  54. package/dist/devops/lib/run-tool.d.ts +2 -2
  55. package/dist/devops/lib/run-tool.js +2 -2
  56. package/dist/devops/lib/types.d.ts +17 -54
  57. package/dist/devops/lib/types.js +2 -2
  58. package/dist/devops/lib/types.js.map +1 -1
  59. package/dist/devops/lib/update.d.ts +0 -15
  60. package/dist/devops/lib/update.js +2 -91
  61. package/dist/devops/lib/update.js.map +1 -1
  62. package/dist/devops/utils/agents.d.ts +0 -28
  63. package/dist/devops/utils/agents.js +0 -462
  64. package/dist/devops/utils/agents.js.map +1 -1
  65. package/dist/devops/utils/config.d.ts +9 -18
  66. package/dist/devops/utils/config.js +7 -20
  67. package/dist/devops/utils/config.js.map +1 -1
  68. package/dist/devops/utils/editor-cache.js +1 -1
  69. package/dist/devops/utils/editor-cache.js.map +1 -1
  70. package/dist/devops/utils/launch-error-dismiss.d.ts +2 -2
  71. package/dist/devops/utils/launch-error-dismiss.js +5 -5
  72. package/dist/devops/utils/manifest.d.ts +2 -2
  73. package/dist/devops/utils/manifest.js +3 -3
  74. package/dist/devops/utils/manifest.js.map +1 -1
  75. package/dist/devops/utils/port.d.ts +1 -1
  76. package/dist/devops/utils/port.js +1 -1
  77. package/dist/devops/utils/probe.d.ts +1 -1
  78. package/dist/devops/utils/probe.js +1 -1
  79. package/dist/devops/utils/ui.js +2 -2
  80. package/dist/devops/utils/ui.js.map +1 -1
  81. package/dist/devops/utils/unity-editor.js +4 -4
  82. package/dist/devops/utils/unity-editor.js.map +1 -1
  83. package/dist/devops/utils/unity-hub.js +1 -1
  84. package/dist/devops/utils/unity-hub.js.map +1 -1
  85. package/dist/devops/utils/unity-shutdown.d.ts +1 -1
  86. package/dist/devops/utils/unity-shutdown.js +1 -1
  87. package/dist/devops/utils/update-check.js +3 -3
  88. package/dist/devops/utils/update-check.js.map +1 -1
  89. package/dist/devops/utils/vendor.d.ts +2 -2
  90. package/dist/devops/utils/vendor.js +4 -4
  91. package/dist/devops/utils/vendor.js.map +1 -1
  92. package/dist/generated/tools.js +1 -1
  93. package/dist/generated/tools.js.map +1 -1
  94. package/dist/program.js +3 -3
  95. package/dist/program.js.map +1 -1
  96. package/dist/server/app.d.ts +5 -5
  97. package/dist/server/app.js +3 -3
  98. package/dist/server/app.js.map +1 -1
  99. package/dist/server/index.d.ts +2 -2
  100. package/dist/server/index.js +2 -2
  101. package/dist/server/index.js.map +1 -1
  102. package/dist/server/rest/auth.d.ts +1 -1
  103. package/dist/server/rest/auth.js +3 -3
  104. package/dist/server/rest/auth.js.map +1 -1
  105. package/dist/server/rest/context.d.ts +2 -2
  106. package/dist/server/rest/forward.d.ts +2 -2
  107. package/dist/server/session/store.d.ts +3 -3
  108. package/dist/server/session/store.js +4 -4
  109. package/dist/server/session/store.js.map +1 -1
  110. package/dist/server/stub-client/scenarios.js +8 -8
  111. package/dist/server/stub-client/scenarios.js.map +1 -1
  112. package/dist/server/types.d.ts +11 -11
  113. package/dist/server/types.js +9 -9
  114. package/dist/server/types.js.map +1 -1
  115. package/dist/server/ws/hub.d.ts +8 -8
  116. package/dist/server/ws/hub.js +13 -13
  117. package/dist/server/ws/hub.js.map +1 -1
  118. package/dist/server/ws/rpc.d.ts +3 -3
  119. package/dist/server/ws/rpc.js +4 -4
  120. package/dist/server/ws/rpc.js.map +1 -1
  121. package/dist/transport/index.d.ts +1 -1
  122. package/dist/transport/index.js +3 -3
  123. package/dist/transport/index.js.map +1 -1
  124. package/dist/transport/rest.js +1 -1
  125. package/dist/transport/rest.js.map +1 -1
  126. package/docs/INSTALL.zh-CN.md +2 -3
  127. package/package.json +1 -1
  128. package/skills/uco-setup/SKILL.md +1 -1
  129. package/skills/unity-editor/references/diagnostics.md +24 -24
  130. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Components/ExtensionPanel.cs +379 -379
  131. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/MenuItems.cs +72 -72
  132. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/UnityCopilotProjectSettingsProvider.cs +64 -64
  133. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +372 -372
  134. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  135. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  136. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  137. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +175 -175
  138. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +1 -1
  139. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +368 -368
  140. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +3 -3
  141. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  142. package/dist/commands/devops/login.d.ts +0 -2
  143. package/dist/commands/devops/login.js +0 -29
  144. package/dist/commands/devops/login.js.map +0 -1
  145. package/dist/commands/devops/setup-mcp.d.ts +0 -2
  146. package/dist/commands/devops/setup-mcp.js +0 -41
  147. package/dist/commands/devops/setup-mcp.js.map +0 -1
  148. package/dist/devops/commands-upstream/bootstrap-local.d.ts +0 -17
  149. package/dist/devops/commands-upstream/bootstrap-local.js +0 -87
  150. package/dist/devops/commands-upstream/bootstrap-local.js.map +0 -1
  151. package/dist/devops/commands-upstream/close.d.ts +0 -57
  152. package/dist/devops/commands-upstream/close.js +0 -188
  153. package/dist/devops/commands-upstream/close.js.map +0 -1
  154. package/dist/devops/commands-upstream/configure.d.ts +0 -2
  155. package/dist/devops/commands-upstream/configure.js +0 -87
  156. package/dist/devops/commands-upstream/configure.js.map +0 -1
  157. package/dist/devops/commands-upstream/create-project.d.ts +0 -2
  158. package/dist/devops/commands-upstream/create-project.js +0 -43
  159. package/dist/devops/commands-upstream/create-project.js.map +0 -1
  160. package/dist/devops/commands-upstream/install-plugin.d.ts +0 -2
  161. package/dist/devops/commands-upstream/install-plugin.js +0 -64
  162. package/dist/devops/commands-upstream/install-plugin.js.map +0 -1
  163. package/dist/devops/commands-upstream/install-unity.d.ts +0 -2
  164. package/dist/devops/commands-upstream/install-unity.js +0 -72
  165. package/dist/devops/commands-upstream/install-unity.js.map +0 -1
  166. package/dist/devops/commands-upstream/login.d.ts +0 -2
  167. package/dist/devops/commands-upstream/login.js +0 -33
  168. package/dist/devops/commands-upstream/login.js.map +0 -1
  169. package/dist/devops/commands-upstream/open.d.ts +0 -32
  170. package/dist/devops/commands-upstream/open.js +0 -282
  171. package/dist/devops/commands-upstream/open.js.map +0 -1
  172. package/dist/devops/commands-upstream/remove-plugin.d.ts +0 -2
  173. package/dist/devops/commands-upstream/remove-plugin.js +0 -41
  174. package/dist/devops/commands-upstream/remove-plugin.js.map +0 -1
  175. package/dist/devops/commands-upstream/run-system-tool.d.ts +0 -1
  176. package/dist/devops/commands-upstream/run-system-tool.js +0 -13
  177. package/dist/devops/commands-upstream/run-system-tool.js.map +0 -1
  178. package/dist/devops/commands-upstream/run-tool-builder.d.ts +0 -25
  179. package/dist/devops/commands-upstream/run-tool-builder.js +0 -125
  180. package/dist/devops/commands-upstream/run-tool-builder.js.map +0 -1
  181. package/dist/devops/commands-upstream/run-tool.d.ts +0 -1
  182. package/dist/devops/commands-upstream/run-tool.js +0 -13
  183. package/dist/devops/commands-upstream/run-tool.js.map +0 -1
  184. package/dist/devops/commands-upstream/setup-mcp.d.ts +0 -2
  185. package/dist/devops/commands-upstream/setup-mcp.js +0 -68
  186. package/dist/devops/commands-upstream/setup-mcp.js.map +0 -1
  187. package/dist/devops/commands-upstream/setup-skills.d.ts +0 -2
  188. package/dist/devops/commands-upstream/setup-skills.js +0 -167
  189. package/dist/devops/commands-upstream/setup-skills.js.map +0 -1
  190. package/dist/devops/commands-upstream/status.d.ts +0 -2
  191. package/dist/devops/commands-upstream/status.js +0 -87
  192. package/dist/devops/commands-upstream/status.js.map +0 -1
  193. package/dist/devops/commands-upstream/update.d.ts +0 -2
  194. package/dist/devops/commands-upstream/update.js +0 -77
  195. package/dist/devops/commands-upstream/update.js.map +0 -1
  196. package/dist/devops/commands-upstream/wait-for-ready.d.ts +0 -2
  197. package/dist/devops/commands-upstream/wait-for-ready.js +0 -93
  198. package/dist/devops/commands-upstream/wait-for-ready.js.map +0 -1
  199. package/dist/devops/lib/setup-mcp.d.ts +0 -35
  200. package/dist/devops/lib/setup-mcp.js +0 -172
  201. package/dist/devops/lib/setup-mcp.js.map +0 -1
  202. package/dist/devops/utils/cloud-login.d.ts +0 -7
  203. package/dist/devops/utils/cloud-login.js +0 -56
  204. package/dist/devops/utils/cloud-login.js.map +0 -1
@@ -1,368 +1,368 @@
1
- /*
2
- ┌──────────────────────────────────────────────────────────────────┐
3
- │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
- │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
- │ Copyright (c) 2025 Ivan Murzak │
6
- │ Licensed under the Apache License, Version 2.0. │
7
- │ See the LICENSE file in the project root for more information. │
8
- └──────────────────────────────────────────────────────────────────┘
9
- */
10
-
11
- #nullable enable
12
- using System;
13
- using System.Collections.Generic;
14
- using com.AtelierAI.Uco.Framework.Common.Utils;
15
- using com.AtelierAI.Unity.Copilot.Utils;
16
- using Microsoft.Extensions.Logging;
17
- using static com.AtelierAI.Uco.Framework.Common.Consts.MCP.Server;
18
-
19
- namespace com.AtelierAI.Unity.Copilot.Runtime.Utils
20
- {
21
- /// <summary>
22
- /// Loads MCP plugin configuration from layered sources and (optionally) applies
23
- /// runtime overrides from environment variables and process command-line arguments.
24
- ///
25
- /// Priority (highest wins):
26
- /// 1. Process command-line flags (e.g. <c>--url</c>, <c>--token</c>, <c>--auth</c>,
27
- /// or any <c>--UNITY_MCP_*</c> variant)
28
- /// 2. Process environment variables (<c>UNITY_MCP_*</c>)
29
- /// 3. On-disk config (<c>UserSettings/AI-Game-Developer-Config.json</c>)
30
- /// 4. Built-in defaults
31
- ///
32
- /// Overrides applied via env vars / flags are NEVER persisted back to disk —
33
- /// they are runtime-only. The <see cref="OverrideRecord"/> returned by
34
- /// <see cref="ApplyEnvironmentOverrides"/> captures both the disk-baseline values
35
- /// and the override values so that the editor's Save() flow can restore the
36
- /// baseline before serializing. See <c>UnityCopilotPluginEditor.Save</c>.
37
- /// </summary>
38
- public static class EnvironmentUtils
39
- {
40
- static readonly ILogger _logger = UnityLoggerFactory.LoggerFactory.CreateLogger(nameof(EnvironmentUtils));
41
-
42
- // Environment variable names for MCP connection overrides.
43
- public const string EnvHost = "UNITY_MCP_HOST";
44
- public const string EnvKeepConnected = "UNITY_MCP_KEEP_CONNECTED";
45
- public const string EnvAuthOption = "UNITY_MCP_AUTH_OPTION";
46
- public const string EnvToken = "UNITY_MCP_TOKEN";
47
- public const string EnvTools = "UNITY_MCP_TOOLS";
48
- public const string EnvStartServer = "UNITY_MCP_START_SERVER";
49
- public const string EnvTransport = "UNITY_MCP_TRANSPORT";
50
- public const string EnvConnectionMode = "UNITY_MCP_CONNECTION_MODE";
51
-
52
- // Short flag aliases recognised by the in-plugin command-line parser.
53
- // The CLI tool already translates these to the equivalent env vars before
54
- // launching Unity, but the plugin still parses them directly so
55
- // out-of-band invocations (manual editor launch, IDE config, etc.) work.
56
- public const string FlagUrl = "url";
57
- public const string FlagToken = "token";
58
- public const string FlagAuth = "auth";
59
-
60
- // Field-name keys used by OverrideRecord to track which fields were overridden
61
- // by env/flags so the Save flow can restore disk-baseline values before writing.
62
- public const string FieldHost = nameof(UnityCopilotPlugin.UnityConnectionConfig.LocalHost);
63
- public const string FieldKeepConnected = nameof(UnityCopilotPlugin.UnityConnectionConfig.KeepConnected);
64
- public const string FieldAuthOption = nameof(UnityCopilotPlugin.UnityConnectionConfig.AuthOption);
65
- public const string FieldLocalToken = nameof(UnityCopilotPlugin.UnityConnectionConfig.LocalToken);
66
- public const string FieldTools = nameof(UnityCopilotPlugin.UnityConnectionConfig.EnabledToolsOverride);
67
- public const string FieldStartServer = nameof(UnityCopilotPlugin.UnityConnectionConfig.KeepServerRunning);
68
- public const string FieldTransport = nameof(UnityCopilotPlugin.UnityConnectionConfig.TransportMethod);
69
- public const string FieldConnectionMode = nameof(UnityCopilotPlugin.UnityConnectionConfig.ConnectionMode);
70
-
71
- /// <summary>
72
- /// Captures, per overridden field, the disk-baseline value (what was read from JSON)
73
- /// and the runtime override value applied on top. Allows <c>Save()</c> to round-trip
74
- /// the baseline to disk while keeping the runtime override on the in-memory config.
75
- /// Empty when no env/flag overrides were detected.
76
- /// </summary>
77
- public sealed class OverrideRecord
78
- {
79
- readonly Dictionary<string, object?> _baselineValues = new();
80
- readonly Dictionary<string, object?> _overrideValues = new();
81
-
82
- public IReadOnlyDictionary<string, object?> BaselineValues => _baselineValues;
83
- public IReadOnlyDictionary<string, object?> OverrideValues => _overrideValues;
84
-
85
- public bool HasAny => _baselineValues.Count > 0;
86
- public bool Contains(string fieldName) => _baselineValues.ContainsKey(fieldName);
87
-
88
- internal void Track(string fieldName, object? baselineValue, object? overrideValue)
89
- {
90
- _baselineValues[fieldName] = baselineValue;
91
- _overrideValues[fieldName] = overrideValue;
92
- }
93
- }
94
-
95
- /// <summary>
96
- /// Checks if the current environment is a CI environment.
97
- /// </summary>
98
- public static bool IsCi()
99
- {
100
- var commandLineArgs = ArgsUtils.ParseCommandLineArguments();
101
-
102
- var ci = commandLineArgs.GetValueOrDefault("CI") ?? Environment.GetEnvironmentVariable("CI");
103
- var gha = commandLineArgs.GetValueOrDefault("GITHUB_ACTIONS") ?? Environment.GetEnvironmentVariable("GITHUB_ACTIONS");
104
- var az = commandLineArgs.GetValueOrDefault("TF_BUILD") ?? Environment.GetEnvironmentVariable("TF_BUILD"); // Azure Pipelines
105
-
106
- return string.Equals(ci?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase)
107
- || string.Equals(gha?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase)
108
- || string.Equals(az?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase);
109
- }
110
-
111
- /// <summary>
112
- /// Checks if the Editor was launched with -batchmode (COCli-13).
113
- /// Batchmode launches without a configured server (offline builds,
114
- /// command-line automation) must not run the connection retry loop —
115
- /// its warnings would pollute BuildReport output.
116
- /// </summary>
117
- public static bool IsBatchMode()
118
- => IsBatchMode(UnityEngine.Application.isBatchMode, ArgsUtils.ParseCommandLineArguments());
119
-
120
- /// <summary>Test-friendly overload taking the parsed command-line args.</summary>
121
- public static bool IsBatchMode(
122
- bool applicationIsBatchMode,
123
- IReadOnlyDictionary<string, string> commandLineArgs)
124
- => applicationIsBatchMode || commandLineArgs.ContainsKey("batchmode");
125
-
126
- /// <summary>
127
- /// Applies environment-variable and command-line-argument overrides to the given config.
128
- /// Args (highest priority) override env vars, env vars override the disk-baseline values
129
- /// already present on <paramref name="config"/>. Returns an <see cref="OverrideRecord"/>
130
- /// that captures the disk-baseline value for each field that was overridden.
131
- /// </summary>
132
- public static OverrideRecord ApplyEnvironmentOverrides(UnityCopilotPlugin.UnityConnectionConfig config)
133
- {
134
- var args = ArgsUtils.ParseCommandLineArguments();
135
- return ApplyEnvironmentOverrides(config, args, Environment.GetEnvironmentVariable);
136
- }
137
-
138
- /// <summary>
139
- /// Test-friendly overload. <paramref name="argReader"/> is the parsed command-line
140
- /// arg dictionary (keys without leading dashes); <paramref name="envReader"/> resolves
141
- /// environment variables. Either may be empty / always-null to disable that source.
142
- /// </summary>
143
- public static OverrideRecord ApplyEnvironmentOverrides(
144
- UnityCopilotPlugin.UnityConnectionConfig config,
145
- IReadOnlyDictionary<string, string> argReader,
146
- Func<string, string?> envReader)
147
- {
148
- if (config == null) throw new ArgumentNullException(nameof(config));
149
- if (argReader == null) throw new ArgumentNullException(nameof(argReader));
150
- if (envReader == null) throw new ArgumentNullException(nameof(envReader));
151
-
152
- var record = new OverrideRecord();
153
-
154
- // Resolve a value with priority: short-flag-alias (highest) > UNITY_MCP_* env-style flag > env var.
155
- // Returns null if no source provided a non-empty value.
156
- string? Resolve(string envKey, string? flagAlias)
157
- {
158
- if (!string.IsNullOrEmpty(flagAlias)
159
- && argReader.TryGetValue(flagAlias, out var flagValue)
160
- && !string.IsNullOrWhiteSpace(flagValue))
161
- {
162
- return flagValue;
163
- }
164
- if (argReader.TryGetValue(envKey, out var envFlagValue)
165
- && !string.IsNullOrWhiteSpace(envFlagValue))
166
- {
167
- return envFlagValue;
168
- }
169
- var envValue = envReader(envKey);
170
- if (!string.IsNullOrWhiteSpace(envValue))
171
- return envValue;
172
- return null;
173
- }
174
-
175
- string Sanitize(string raw) => raw.Trim().Trim('"');
176
-
177
- // UNITY_MCP_HOST / --url override the local server host.
178
- string? sanitizedHost = null;
179
- var rawHost = Resolve(EnvHost, FlagUrl);
180
- if (rawHost != null)
181
- {
182
- var host = Sanitize(rawHost).TrimEnd('/');
183
- if (host.Length > 0)
184
- {
185
- sanitizedHost = host;
186
- if (!string.Equals(host, config.LocalHost, StringComparison.Ordinal))
187
- {
188
- record.Track(FieldHost, config.LocalHost, host);
189
- config.LocalHost = host;
190
- _logger.LogInformation("[MCP] Override: {Key}={Value}", FieldHost, host);
191
- }
192
- }
193
- }
194
-
195
- // Loopback URLs without an explicit mode infer Custom (worktree / local-dev).
196
- // Remote URLs without an explicit mode leave the disk-baseline ConnectionMode untouched.
197
- var rawMode = Resolve(EnvConnectionMode, flagAlias: null);
198
- ConnectionMode? targetMode = null;
199
- if (rawMode != null && Enum.TryParse<ConnectionMode>(Sanitize(rawMode), ignoreCase: true, out var explicitMode))
200
- {
201
- targetMode = explicitMode;
202
- }
203
- else if (sanitizedHost != null && IsLoopbackUrl(sanitizedHost))
204
- {
205
- targetMode = ConnectionMode.Custom;
206
- }
207
- if (targetMode.HasValue && targetMode.Value != config.ConnectionMode)
208
- {
209
- record.Track(FieldConnectionMode, config.ConnectionMode, targetMode.Value);
210
- config.ConnectionMode = targetMode.Value;
211
- _logger.LogInformation("[MCP] Override: {Key}={Value}", FieldConnectionMode, targetMode.Value);
212
- }
213
-
214
- var rawKeep = Resolve(EnvKeepConnected, flagAlias: null);
215
- if (rawKeep != null && bool.TryParse(Sanitize(rawKeep), out var keep) && keep != config.KeepConnected)
216
- {
217
- record.Track(FieldKeepConnected, config.KeepConnected, keep);
218
- config.KeepConnected = keep;
219
- _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvKeepConnected, keep);
220
- }
221
-
222
- var rawAuth = Resolve(EnvAuthOption, FlagAuth);
223
- if (rawAuth != null
224
- && Enum.TryParse<AuthOption>(Sanitize(rawAuth), ignoreCase: true, out var ao)
225
- && ao != config.AuthOption)
226
- {
227
- record.Track(FieldAuthOption, config.AuthOption, ao);
228
- config.AuthOption = ao;
229
- _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvAuthOption, ao);
230
- }
231
-
232
- // We track the specific
233
- // backing field rather than the abstract Token property because only the backing
234
- // fields are serialised — restoring the baseline must target the same field that
235
- // was clobbered.
236
- var rawToken = Resolve(EnvToken, FlagToken);
237
- if (rawToken != null)
238
- {
239
- var token = Sanitize(rawToken);
240
- if (!string.Equals(token, config.LocalToken, StringComparison.Ordinal))
241
- {
242
- record.Track(FieldLocalToken, config.LocalToken, token);
243
- config.LocalToken = token;
244
- _logger.LogInformation("[MCP] Override: {Key}=*** (LocalToken)", EnvToken);
245
- }
246
- }
247
-
248
- var rawTransport = Resolve(EnvTransport, flagAlias: null);
249
- if (rawTransport != null
250
- && Enum.TryParse<TransportMethod>(Sanitize(rawTransport), ignoreCase: true, out var tm)
251
- && tm != config.TransportMethod)
252
- {
253
- record.Track(FieldTransport, config.TransportMethod, tm);
254
- config.TransportMethod = tm;
255
- _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvTransport, tm);
256
- }
257
-
258
- var rawStart = Resolve(EnvStartServer, flagAlias: null);
259
- if (rawStart != null && bool.TryParse(Sanitize(rawStart), out var ss) && ss != config.KeepServerRunning)
260
- {
261
- record.Track(FieldStartServer, config.KeepServerRunning, ss);
262
- config.KeepServerRunning = ss;
263
- _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvStartServer, ss);
264
- }
265
-
266
- // EnabledToolsOverride is [JsonIgnore] so it is never persisted regardless of the
267
- // baseline-restoration logic; we still track it for completeness.
268
- var rawTools = Resolve(EnvTools, flagAlias: null);
269
- if (rawTools != null)
270
- {
271
- var ids = rawTools.Split(',', StringSplitOptions.RemoveEmptyEntries);
272
- var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
273
- var trimmed = new List<string>(ids.Length);
274
- foreach (var id in ids)
275
- {
276
- var value = Sanitize(id);
277
- if (!string.IsNullOrWhiteSpace(value) && seen.Add(value))
278
- trimmed.Add(value);
279
- }
280
- record.Track(FieldTools, config.EnabledToolsOverride, trimmed);
281
- config.EnabledToolsOverride = trimmed;
282
- _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvTools, rawTools.Trim());
283
- }
284
-
285
- return record;
286
- }
287
-
288
- /// <summary>
289
- /// Applies the BaselineValues from <paramref name="record"/> to <paramref name="config"/>
290
- /// (i.e. reverses runtime overrides so the in-memory config reflects what is on disk).
291
- /// Used by the editor's Save() flow to ensure runtime-only overrides are not persisted.
292
- /// </summary>
293
- public static void ApplyBaseline(UnityCopilotPlugin.UnityConnectionConfig config, OverrideRecord record)
294
- {
295
- if (config == null) throw new ArgumentNullException(nameof(config));
296
- if (record == null) throw new ArgumentNullException(nameof(record));
297
- ApplyValues(config, record.BaselineValues);
298
- }
299
-
300
- /// <summary>
301
- /// Re-applies the OverrideValues from <paramref name="record"/> to <paramref name="config"/>
302
- /// (i.e. restores runtime overrides after a temporary baseline-restore).
303
- /// </summary>
304
- public static void ApplyOverrides(UnityCopilotPlugin.UnityConnectionConfig config, OverrideRecord record)
305
- {
306
- if (config == null) throw new ArgumentNullException(nameof(config));
307
- if (record == null) throw new ArgumentNullException(nameof(record));
308
- ApplyValues(config, record.OverrideValues);
309
- }
310
-
311
- static void ApplyValues(UnityCopilotPlugin.UnityConnectionConfig config, IReadOnlyDictionary<string, object?> values)
312
- {
313
- foreach (var kvp in values)
314
- {
315
- switch (kvp.Key)
316
- {
317
- case FieldHost:
318
- config.LocalHost = (string?)kvp.Value ?? UnityCopilotPlugin.UnityConnectionConfig.DefaultHost;
319
- break;
320
- case FieldKeepConnected:
321
- if (kvp.Value is bool kc) config.KeepConnected = kc;
322
- break;
323
- case FieldAuthOption:
324
- if (kvp.Value is AuthOption ao) config.AuthOption = ao;
325
- break;
326
- case FieldLocalToken:
327
- config.LocalToken = (string?)kvp.Value;
328
- break;
329
- case FieldTools:
330
- config.EnabledToolsOverride = (List<string>?)kvp.Value;
331
- break;
332
- case FieldStartServer:
333
- if (kvp.Value is bool ss) config.KeepServerRunning = ss;
334
- break;
335
- case FieldTransport:
336
- if (kvp.Value is TransportMethod tm) config.TransportMethod = tm;
337
- break;
338
- case FieldConnectionMode:
339
- if (kvp.Value is ConnectionMode cm) config.ConnectionMode = cm;
340
- break;
341
- default:
342
- // Fail loudly if a new Field* constant is added to Track but forgotten here —
343
- // a silent miss would let runtime overrides leak to disk on Save.
344
- throw new InvalidOperationException($"Unhandled override field: {kvp.Key}");
345
- }
346
- }
347
- }
348
-
349
- /// <summary>
350
- /// Returns true if the given URL targets a loopback host
351
- /// (<c>localhost</c>, <c>127.0.0.0/8</c>, or IPv6 <c>::1</c>).
352
- /// Used to infer <see cref="ConnectionMode.Custom"/> when a worktree-style local URL is supplied.
353
- /// </summary>
354
- public static bool IsLoopbackUrl(string url)
355
- {
356
- if (string.IsNullOrWhiteSpace(url))
357
- return false;
358
- if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
359
- return false;
360
- var host = uri.Host;
361
- if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
362
- return true;
363
- if (System.Net.IPAddress.TryParse(host, out var ip))
364
- return System.Net.IPAddress.IsLoopback(ip);
365
- return false;
366
- }
367
- }
368
- }
1
+ /*
2
+ ┌──────────────────────────────────────────────────────────────────┐
3
+ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
+ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
+ │ Copyright (c) 2025 Ivan Murzak │
6
+ │ Licensed under the Apache License, Version 2.0. │
7
+ │ See the LICENSE file in the project root for more information. │
8
+ └──────────────────────────────────────────────────────────────────┘
9
+ */
10
+
11
+ #nullable enable
12
+ using System;
13
+ using System.Collections.Generic;
14
+ using com.AtelierAI.Uco.Framework.Common.Utils;
15
+ using com.AtelierAI.Unity.Copilot.Utils;
16
+ using Microsoft.Extensions.Logging;
17
+ using static com.AtelierAI.Uco.Framework.Common.Consts.MCP.Server;
18
+
19
+ namespace com.AtelierAI.Unity.Copilot.Runtime.Utils
20
+ {
21
+ /// <summary>
22
+ /// Loads MCP plugin configuration from layered sources and (optionally) applies
23
+ /// runtime overrides from environment variables and process command-line arguments.
24
+ ///
25
+ /// Priority (highest wins):
26
+ /// 1. Process command-line flags (e.g. <c>--url</c>, <c>--token</c>, <c>--auth</c>,
27
+ /// or any <c>--UNITY_COPILOT_*</c> variant)
28
+ /// 2. Process environment variables (<c>UNITY_COPILOT_*</c>)
29
+ /// 3. On-disk config (<c>UserSettings/AI-Game-Developer-Config.json</c>)
30
+ /// 4. Built-in defaults
31
+ ///
32
+ /// Overrides applied via env vars / flags are NEVER persisted back to disk —
33
+ /// they are runtime-only. The <see cref="OverrideRecord"/> returned by
34
+ /// <see cref="ApplyEnvironmentOverrides"/> captures both the disk-baseline values
35
+ /// and the override values so that the editor's Save() flow can restore the
36
+ /// baseline before serializing. See <c>UnityCopilotPluginEditor.Save</c>.
37
+ /// </summary>
38
+ public static class EnvironmentUtils
39
+ {
40
+ static readonly ILogger _logger = UnityLoggerFactory.LoggerFactory.CreateLogger(nameof(EnvironmentUtils));
41
+
42
+ // Environment variable names for MCP connection overrides.
43
+ public const string EnvHost = "UNITY_COPILOT_HOST";
44
+ public const string EnvKeepConnected = "UNITY_COPILOT_KEEP_CONNECTED";
45
+ public const string EnvAuthOption = "UNITY_COPILOT_AUTH_OPTION";
46
+ public const string EnvToken = "UNITY_COPILOT_TOKEN";
47
+ public const string EnvTools = "UNITY_COPILOT_TOOLS";
48
+ public const string EnvStartServer = "UNITY_COPILOT_START_SERVER";
49
+ public const string EnvTransport = "UNITY_COPILOT_TRANSPORT";
50
+ public const string EnvConnectionMode = "UNITY_COPILOT_CONNECTION_MODE";
51
+
52
+ // Short flag aliases recognised by the in-plugin command-line parser.
53
+ // The CLI tool already translates these to the equivalent env vars before
54
+ // launching Unity, but the plugin still parses them directly so
55
+ // out-of-band invocations (manual editor launch, IDE config, etc.) work.
56
+ public const string FlagUrl = "url";
57
+ public const string FlagToken = "token";
58
+ public const string FlagAuth = "auth";
59
+
60
+ // Field-name keys used by OverrideRecord to track which fields were overridden
61
+ // by env/flags so the Save flow can restore disk-baseline values before writing.
62
+ public const string FieldHost = nameof(UnityCopilotPlugin.UnityConnectionConfig.LocalHost);
63
+ public const string FieldKeepConnected = nameof(UnityCopilotPlugin.UnityConnectionConfig.KeepConnected);
64
+ public const string FieldAuthOption = nameof(UnityCopilotPlugin.UnityConnectionConfig.AuthOption);
65
+ public const string FieldLocalToken = nameof(UnityCopilotPlugin.UnityConnectionConfig.LocalToken);
66
+ public const string FieldTools = nameof(UnityCopilotPlugin.UnityConnectionConfig.EnabledToolsOverride);
67
+ public const string FieldStartServer = nameof(UnityCopilotPlugin.UnityConnectionConfig.KeepServerRunning);
68
+ public const string FieldTransport = nameof(UnityCopilotPlugin.UnityConnectionConfig.TransportMethod);
69
+ public const string FieldConnectionMode = nameof(UnityCopilotPlugin.UnityConnectionConfig.ConnectionMode);
70
+
71
+ /// <summary>
72
+ /// Captures, per overridden field, the disk-baseline value (what was read from JSON)
73
+ /// and the runtime override value applied on top. Allows <c>Save()</c> to round-trip
74
+ /// the baseline to disk while keeping the runtime override on the in-memory config.
75
+ /// Empty when no env/flag overrides were detected.
76
+ /// </summary>
77
+ public sealed class OverrideRecord
78
+ {
79
+ readonly Dictionary<string, object?> _baselineValues = new();
80
+ readonly Dictionary<string, object?> _overrideValues = new();
81
+
82
+ public IReadOnlyDictionary<string, object?> BaselineValues => _baselineValues;
83
+ public IReadOnlyDictionary<string, object?> OverrideValues => _overrideValues;
84
+
85
+ public bool HasAny => _baselineValues.Count > 0;
86
+ public bool Contains(string fieldName) => _baselineValues.ContainsKey(fieldName);
87
+
88
+ internal void Track(string fieldName, object? baselineValue, object? overrideValue)
89
+ {
90
+ _baselineValues[fieldName] = baselineValue;
91
+ _overrideValues[fieldName] = overrideValue;
92
+ }
93
+ }
94
+
95
+ /// <summary>
96
+ /// Checks if the current environment is a CI environment.
97
+ /// </summary>
98
+ public static bool IsCi()
99
+ {
100
+ var commandLineArgs = ArgsUtils.ParseCommandLineArguments();
101
+
102
+ var ci = commandLineArgs.GetValueOrDefault("CI") ?? Environment.GetEnvironmentVariable("CI");
103
+ var gha = commandLineArgs.GetValueOrDefault("GITHUB_ACTIONS") ?? Environment.GetEnvironmentVariable("GITHUB_ACTIONS");
104
+ var az = commandLineArgs.GetValueOrDefault("TF_BUILD") ?? Environment.GetEnvironmentVariable("TF_BUILD"); // Azure Pipelines
105
+
106
+ return string.Equals(ci?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase)
107
+ || string.Equals(gha?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase)
108
+ || string.Equals(az?.Trim()?.Trim('"'), "true", StringComparison.OrdinalIgnoreCase);
109
+ }
110
+
111
+ /// <summary>
112
+ /// Checks if the Editor was launched with -batchmode (COCli-13).
113
+ /// Batchmode launches without a configured server (offline builds,
114
+ /// command-line automation) must not run the connection retry loop —
115
+ /// its warnings would pollute BuildReport output.
116
+ /// </summary>
117
+ public static bool IsBatchMode()
118
+ => IsBatchMode(UnityEngine.Application.isBatchMode, ArgsUtils.ParseCommandLineArguments());
119
+
120
+ /// <summary>Test-friendly overload taking the parsed command-line args.</summary>
121
+ public static bool IsBatchMode(
122
+ bool applicationIsBatchMode,
123
+ IReadOnlyDictionary<string, string> commandLineArgs)
124
+ => applicationIsBatchMode || commandLineArgs.ContainsKey("batchmode");
125
+
126
+ /// <summary>
127
+ /// Applies environment-variable and command-line-argument overrides to the given config.
128
+ /// Args (highest priority) override env vars, env vars override the disk-baseline values
129
+ /// already present on <paramref name="config"/>. Returns an <see cref="OverrideRecord"/>
130
+ /// that captures the disk-baseline value for each field that was overridden.
131
+ /// </summary>
132
+ public static OverrideRecord ApplyEnvironmentOverrides(UnityCopilotPlugin.UnityConnectionConfig config)
133
+ {
134
+ var args = ArgsUtils.ParseCommandLineArguments();
135
+ return ApplyEnvironmentOverrides(config, args, Environment.GetEnvironmentVariable);
136
+ }
137
+
138
+ /// <summary>
139
+ /// Test-friendly overload. <paramref name="argReader"/> is the parsed command-line
140
+ /// arg dictionary (keys without leading dashes); <paramref name="envReader"/> resolves
141
+ /// environment variables. Either may be empty / always-null to disable that source.
142
+ /// </summary>
143
+ public static OverrideRecord ApplyEnvironmentOverrides(
144
+ UnityCopilotPlugin.UnityConnectionConfig config,
145
+ IReadOnlyDictionary<string, string> argReader,
146
+ Func<string, string?> envReader)
147
+ {
148
+ if (config == null) throw new ArgumentNullException(nameof(config));
149
+ if (argReader == null) throw new ArgumentNullException(nameof(argReader));
150
+ if (envReader == null) throw new ArgumentNullException(nameof(envReader));
151
+
152
+ var record = new OverrideRecord();
153
+
154
+ // Resolve a value with priority: short-flag-alias (highest) > UNITY_COPILOT_* env-style flag > env var.
155
+ // Returns null if no source provided a non-empty value.
156
+ string? Resolve(string envKey, string? flagAlias)
157
+ {
158
+ if (!string.IsNullOrEmpty(flagAlias)
159
+ && argReader.TryGetValue(flagAlias, out var flagValue)
160
+ && !string.IsNullOrWhiteSpace(flagValue))
161
+ {
162
+ return flagValue;
163
+ }
164
+ if (argReader.TryGetValue(envKey, out var envFlagValue)
165
+ && !string.IsNullOrWhiteSpace(envFlagValue))
166
+ {
167
+ return envFlagValue;
168
+ }
169
+ var envValue = envReader(envKey);
170
+ if (!string.IsNullOrWhiteSpace(envValue))
171
+ return envValue;
172
+ return null;
173
+ }
174
+
175
+ string Sanitize(string raw) => raw.Trim().Trim('"');
176
+
177
+ // UNITY_COPILOT_HOST / --url override the local server host.
178
+ string? sanitizedHost = null;
179
+ var rawHost = Resolve(EnvHost, FlagUrl);
180
+ if (rawHost != null)
181
+ {
182
+ var host = Sanitize(rawHost).TrimEnd('/');
183
+ if (host.Length > 0)
184
+ {
185
+ sanitizedHost = host;
186
+ if (!string.Equals(host, config.LocalHost, StringComparison.Ordinal))
187
+ {
188
+ record.Track(FieldHost, config.LocalHost, host);
189
+ config.LocalHost = host;
190
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", FieldHost, host);
191
+ }
192
+ }
193
+ }
194
+
195
+ // Loopback URLs without an explicit mode infer Custom (worktree / local-dev).
196
+ // Remote URLs without an explicit mode leave the disk-baseline ConnectionMode untouched.
197
+ var rawMode = Resolve(EnvConnectionMode, flagAlias: null);
198
+ ConnectionMode? targetMode = null;
199
+ if (rawMode != null && Enum.TryParse<ConnectionMode>(Sanitize(rawMode), ignoreCase: true, out var explicitMode))
200
+ {
201
+ targetMode = explicitMode;
202
+ }
203
+ else if (sanitizedHost != null && IsLoopbackUrl(sanitizedHost))
204
+ {
205
+ targetMode = ConnectionMode.Custom;
206
+ }
207
+ if (targetMode.HasValue && targetMode.Value != config.ConnectionMode)
208
+ {
209
+ record.Track(FieldConnectionMode, config.ConnectionMode, targetMode.Value);
210
+ config.ConnectionMode = targetMode.Value;
211
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", FieldConnectionMode, targetMode.Value);
212
+ }
213
+
214
+ var rawKeep = Resolve(EnvKeepConnected, flagAlias: null);
215
+ if (rawKeep != null && bool.TryParse(Sanitize(rawKeep), out var keep) && keep != config.KeepConnected)
216
+ {
217
+ record.Track(FieldKeepConnected, config.KeepConnected, keep);
218
+ config.KeepConnected = keep;
219
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvKeepConnected, keep);
220
+ }
221
+
222
+ var rawAuth = Resolve(EnvAuthOption, FlagAuth);
223
+ if (rawAuth != null
224
+ && Enum.TryParse<AuthOption>(Sanitize(rawAuth), ignoreCase: true, out var ao)
225
+ && ao != config.AuthOption)
226
+ {
227
+ record.Track(FieldAuthOption, config.AuthOption, ao);
228
+ config.AuthOption = ao;
229
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvAuthOption, ao);
230
+ }
231
+
232
+ // We track the specific
233
+ // backing field rather than the abstract Token property because only the backing
234
+ // fields are serialised — restoring the baseline must target the same field that
235
+ // was clobbered.
236
+ var rawToken = Resolve(EnvToken, FlagToken);
237
+ if (rawToken != null)
238
+ {
239
+ var token = Sanitize(rawToken);
240
+ if (!string.Equals(token, config.LocalToken, StringComparison.Ordinal))
241
+ {
242
+ record.Track(FieldLocalToken, config.LocalToken, token);
243
+ config.LocalToken = token;
244
+ _logger.LogInformation("[MCP] Override: {Key}=*** (LocalToken)", EnvToken);
245
+ }
246
+ }
247
+
248
+ var rawTransport = Resolve(EnvTransport, flagAlias: null);
249
+ if (rawTransport != null
250
+ && Enum.TryParse<TransportMethod>(Sanitize(rawTransport), ignoreCase: true, out var tm)
251
+ && tm != config.TransportMethod)
252
+ {
253
+ record.Track(FieldTransport, config.TransportMethod, tm);
254
+ config.TransportMethod = tm;
255
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvTransport, tm);
256
+ }
257
+
258
+ var rawStart = Resolve(EnvStartServer, flagAlias: null);
259
+ if (rawStart != null && bool.TryParse(Sanitize(rawStart), out var ss) && ss != config.KeepServerRunning)
260
+ {
261
+ record.Track(FieldStartServer, config.KeepServerRunning, ss);
262
+ config.KeepServerRunning = ss;
263
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvStartServer, ss);
264
+ }
265
+
266
+ // EnabledToolsOverride is [JsonIgnore] so it is never persisted regardless of the
267
+ // baseline-restoration logic; we still track it for completeness.
268
+ var rawTools = Resolve(EnvTools, flagAlias: null);
269
+ if (rawTools != null)
270
+ {
271
+ var ids = rawTools.Split(',', StringSplitOptions.RemoveEmptyEntries);
272
+ var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
273
+ var trimmed = new List<string>(ids.Length);
274
+ foreach (var id in ids)
275
+ {
276
+ var value = Sanitize(id);
277
+ if (!string.IsNullOrWhiteSpace(value) && seen.Add(value))
278
+ trimmed.Add(value);
279
+ }
280
+ record.Track(FieldTools, config.EnabledToolsOverride, trimmed);
281
+ config.EnabledToolsOverride = trimmed;
282
+ _logger.LogInformation("[MCP] Override: {Key}={Value}", EnvTools, rawTools.Trim());
283
+ }
284
+
285
+ return record;
286
+ }
287
+
288
+ /// <summary>
289
+ /// Applies the BaselineValues from <paramref name="record"/> to <paramref name="config"/>
290
+ /// (i.e. reverses runtime overrides so the in-memory config reflects what is on disk).
291
+ /// Used by the editor's Save() flow to ensure runtime-only overrides are not persisted.
292
+ /// </summary>
293
+ public static void ApplyBaseline(UnityCopilotPlugin.UnityConnectionConfig config, OverrideRecord record)
294
+ {
295
+ if (config == null) throw new ArgumentNullException(nameof(config));
296
+ if (record == null) throw new ArgumentNullException(nameof(record));
297
+ ApplyValues(config, record.BaselineValues);
298
+ }
299
+
300
+ /// <summary>
301
+ /// Re-applies the OverrideValues from <paramref name="record"/> to <paramref name="config"/>
302
+ /// (i.e. restores runtime overrides after a temporary baseline-restore).
303
+ /// </summary>
304
+ public static void ApplyOverrides(UnityCopilotPlugin.UnityConnectionConfig config, OverrideRecord record)
305
+ {
306
+ if (config == null) throw new ArgumentNullException(nameof(config));
307
+ if (record == null) throw new ArgumentNullException(nameof(record));
308
+ ApplyValues(config, record.OverrideValues);
309
+ }
310
+
311
+ static void ApplyValues(UnityCopilotPlugin.UnityConnectionConfig config, IReadOnlyDictionary<string, object?> values)
312
+ {
313
+ foreach (var kvp in values)
314
+ {
315
+ switch (kvp.Key)
316
+ {
317
+ case FieldHost:
318
+ config.LocalHost = (string?)kvp.Value ?? UnityCopilotPlugin.UnityConnectionConfig.DefaultHost;
319
+ break;
320
+ case FieldKeepConnected:
321
+ if (kvp.Value is bool kc) config.KeepConnected = kc;
322
+ break;
323
+ case FieldAuthOption:
324
+ if (kvp.Value is AuthOption ao) config.AuthOption = ao;
325
+ break;
326
+ case FieldLocalToken:
327
+ config.LocalToken = (string?)kvp.Value;
328
+ break;
329
+ case FieldTools:
330
+ config.EnabledToolsOverride = (List<string>?)kvp.Value;
331
+ break;
332
+ case FieldStartServer:
333
+ if (kvp.Value is bool ss) config.KeepServerRunning = ss;
334
+ break;
335
+ case FieldTransport:
336
+ if (kvp.Value is TransportMethod tm) config.TransportMethod = tm;
337
+ break;
338
+ case FieldConnectionMode:
339
+ if (kvp.Value is ConnectionMode cm) config.ConnectionMode = cm;
340
+ break;
341
+ default:
342
+ // Fail loudly if a new Field* constant is added to Track but forgotten here —
343
+ // a silent miss would let runtime overrides leak to disk on Save.
344
+ throw new InvalidOperationException($"Unhandled override field: {kvp.Key}");
345
+ }
346
+ }
347
+ }
348
+
349
+ /// <summary>
350
+ /// Returns true if the given URL targets a loopback host
351
+ /// (<c>localhost</c>, <c>127.0.0.0/8</c>, or IPv6 <c>::1</c>).
352
+ /// Used to infer <see cref="ConnectionMode.Custom"/> when a worktree-style local URL is supplied.
353
+ /// </summary>
354
+ public static bool IsLoopbackUrl(string url)
355
+ {
356
+ if (string.IsNullOrWhiteSpace(url))
357
+ return false;
358
+ if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
359
+ return false;
360
+ var host = uri.Host;
361
+ if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
362
+ return true;
363
+ if (System.Net.IPAddress.TryParse(host, out var ip))
364
+ return System.Net.IPAddress.IsLoopback(ip);
365
+ return false;
366
+ }
367
+ }
368
+ }