Files
tyx_AI_xhs/supervisor/Dada.Supervisor/ControlledExternalValidationLauncher.cs
T
suyx 5631ef80f9
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
fix(wp7-02): route stable Gemini through secure broker
2026-08-04 18:55:32 +08:00

132 lines
5.4 KiB
C#

using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace Dada.Supervisor;
internal static partial class ControlledExternalValidationLauncher
{
private static readonly HashSet<string> AllowedModels =
[
"gemini-3.1-flash-image",
"gpt-image-2",
];
private static readonly HashSet<string> ValueOptions =
[
"--max-real-calls",
"--model",
"--run-id",
"--service",
];
private static readonly HashSet<string> SwitchOptions =
[
"--confirm-controlled-real",
"--execute-controlled-real",
];
internal static async Task<int> RunAsync(string[] args, ICredentialStore credentials, CancellationToken cancellationToken = default)
{
var validated = ValidateArguments(args);
var script = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "scripts", "validate-external.mjs"));
if (!File.Exists(script)) throw new InvalidOperationException("external_validator_not_found");
var startInfo = new ProcessStartInfo("node") { WorkingDirectory = Environment.CurrentDirectory };
startInfo.ArgumentList.Add(script);
foreach (var value in validated) startInfo.ArgumentList.Add(value);
startInfo.ArgumentList.Add("--credential-stdin");
var result = await CredentialProcessLauncher.RunToCompletionAsync(startInfo, ChildRole.Worker, credentials, cancellationToken);
if (result.SensitiveOutputDetected || !TrySelectSanitizedJson(result, out var output, out var useError))
{
Console.Error.WriteLine("{\"code\":\"external_validator_output_invalid\",\"real_calls\":0,\"status\":\"failed\"}");
return 1;
}
if (useError) Console.Error.WriteLine(output); else Console.WriteLine(output);
return result.ExitCode;
}
internal static string[] ValidateArguments(string[] args)
{
var values = new Dictionary<string, string>(StringComparer.Ordinal);
var switches = new HashSet<string>(StringComparer.Ordinal);
for (var index = 0; index < args.Length; index++)
{
var option = args[index];
if (SwitchOptions.Contains(option))
{
if (!switches.Add(option)) throw new ArgumentException("external_validator_argument_duplicate");
continue;
}
if (!ValueOptions.Contains(option) || index + 1 >= args.Length || !values.TryAdd(option, args[++index]))
{
throw new ArgumentException("external_validator_argument_invalid");
}
}
if (values.GetValueOrDefault("--service") != "ai-gateway-service-id"
|| !AllowedModels.Contains(values.GetValueOrDefault("--model") ?? string.Empty)
|| !SafeRunId().IsMatch(values.GetValueOrDefault("--run-id") ?? string.Empty)
|| values.GetValueOrDefault("--max-real-calls") != "120"
|| !switches.SetEquals(SwitchOptions))
{
throw new ArgumentException("external_validator_argument_invalid");
}
if (values.Values.Any(value => value.Length == 0 || value.IndexOfAny(['\r', '\n', '\0']) >= 0))
{
throw new ArgumentException("external_validator_argument_invalid");
}
return args.ToArray();
}
private static bool TrySelectSanitizedJson(CredentialProcessResult result, out string output, out bool useError)
{
var stdout = result.StandardOutput.Trim();
var stderr = result.StandardError.Trim();
useError = stdout.Length == 0;
output = useError ? stderr : stdout;
if (output.Length == 0 || (stdout.Length > 0 && stderr.Length > 0)) return false;
try
{
using var document = JsonDocument.Parse(output);
return IsSanitized(document.RootElement);
}
catch (JsonException)
{
return false;
}
}
private static bool IsSanitized(JsonElement element)
{
if (element.ValueKind == JsonValueKind.Object)
{
foreach (var property in element.EnumerateObject())
{
if (ForbiddenKey().IsMatch(property.Name) || property.NameEquals("verified") || !IsSanitized(property.Value)) return false;
}
}
else if (element.ValueKind == JsonValueKind.Array)
{
foreach (var item in element.EnumerateArray()) if (!IsSanitized(item)) return false;
}
else if (element.ValueKind == JsonValueKind.String)
{
var value = element.GetString() ?? string.Empty;
if (WindowsUserPath().IsMatch(value) || BearerValue().IsMatch(value)) return false;
}
return true;
}
[GeneratedRegex("^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$", RegexOptions.CultureInvariant)]
private static partial Regex SafeRunId();
[GeneratedRegex("(?:^|_)(?:absolute_path|authorization|body|credential|image|password|path|prompt|raw|secret|token)(?:_|$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ForbiddenKey();
[GeneratedRegex("[A-Za-z]:\\\\Users\\\\", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex WindowsUserPath();
[GeneratedRegex("(?:Bearer\\s+|\\bsk-[A-Za-z0-9_-]{8,})", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex BearerValue();
}