From c2f89453a259c7f4333784b51271d802853d4230 Mon Sep 17 00:00:00 2001 From: suyx Date: Tue, 4 Aug 2026 15:46:38 +0800 Subject: [PATCH] feat: add secure WP7-02 external executor --- scripts/lib/wp7-02-controlled-executor.mjs | 254 ++++++++++++++++++ supervisor/Dada.Supervisor.Tests/Program.cs | 54 +++- .../ControlledExternalValidationLauncher.cs | 138 ++++++++++ supervisor/Dada.Supervisor/Credentials.cs | 50 ++++ .../Dada.Supervisor/OfflineCommandRouter.cs | 3 +- .../wp7-02-controlled-executor.test.mjs | 31 +++ 6 files changed, 520 insertions(+), 10 deletions(-) create mode 100644 scripts/lib/wp7-02-controlled-executor.mjs create mode 100644 supervisor/Dada.Supervisor/ControlledExternalValidationLauncher.cs diff --git a/scripts/lib/wp7-02-controlled-executor.mjs b/scripts/lib/wp7-02-controlled-executor.mjs new file mode 100644 index 0000000..a204a82 --- /dev/null +++ b/scripts/lib/wp7-02-controlled-executor.mjs @@ -0,0 +1,254 @@ +import { createHash } from "node:crypto"; + +import { WP7_02_MODEL_IDS, buildModelContractPlan } from "./wp7-02-external-contract.mjs"; + +export const WP7_02_CONTROLLED_REAL_LIMIT = 120; + +const ratios = ["3:4", "1:1", "4:3", "9:16"]; +const openAiSizes = Object.freeze({ + "3:4": "1024x1536", + "1:1": "1024x1024", + "4:3": "1536x1024", + "9:16": "1024x1536", +}); +const allowedMimeTypes = new Set(["image/jpeg", "image/png", "image/webp"]); +const forbiddenEvidenceKeys = /(?:^|_)(?:absolute_path|authorization|body|credential|credential_value|image|password|path|prompt|raw|raw_provider_payload|raw_prompt|secret|token)(?:_|$)/i; + +function sha256(value) { + return createHash("sha256").update(value).digest("hex").toUpperCase(); +} + +function assertModelConfig(modelConfig) { + if (!modelConfig || typeof modelConfig !== "object" || !WP7_02_MODEL_IDS.includes(modelConfig.model_id)) { + throw new Error("WP7_02_MODEL_CONFIG_INVALID"); + } + if (!Number.isSafeInteger(modelConfig.config_version) || modelConfig.config_version <= 0) { + throw new Error("WP7_02_MODEL_CONFIG_VERSION_INVALID"); + } + const profile = modelConfig.route_profile; + if (!profile || typeof profile !== "object" || typeof profile.endpoint !== "string" + || !profile.endpoint.startsWith("https://oneapi.intelligrow.cn/") + || !["gemini-native-v1beta", "openai-images-v1"].includes(profile.protocol_version)) { + throw new Error("WP7_02_ROUTE_PROFILE_INVALID"); + } + return modelConfig; +} + +export function buildControlledExecutionPlan(modelConfig) { + const config = assertModelConfig(modelConfig); + const contractPlan = buildModelContractPlan(config.model_id); + const realScenarios = [ + ...ratios.map((ratio) => ({ input: "pure_text", ratio, source: "real_gateway" })), + { input: "reference_image", ratio: "1:1", source: "real_gateway" }, + ]; + return { + config_version: config.config_version, + error_scenarios: contractPlan.error_categories.map((name) => ({ + expected: contractPlan.error_expectations[name], name, source: "deterministic_local", + })), + execution_modes: [ + { mode: "sync", source: "real_gateway" }, + { mode: "async", source: "deterministic_local" }, + { mode: "poll", source: "deterministic_local" }, + ], + model_id: config.model_id, + planned_real_calls: realScenarios.length, + quota_impact: "authorized_test_key_up_to_120_requests", + real_scenarios: realScenarios, + response_checks: ["single_image", "mime", "dimensions", "sanitized_usage", "evidence_hash"], + state_scenarios: contractPlan.state_checks.map((name) => ({ name, source: "deterministic_local" })), + }; +} + +export function buildProviderRequest({ modelConfig, prompt, ratio, reference }) { + const config = assertModelConfig(modelConfig); + if (typeof prompt !== "string" || !prompt.trim() || !ratios.includes(ratio)) throw new Error("WP7_02_REQUEST_FIXTURE_INVALID"); + if (reference && (!Buffer.isBuffer(reference.bytes) || reference.bytes.length === 0 || !allowedMimeTypes.has(reference.mime_type))) { + throw new Error("WP7_02_REFERENCE_FIXTURE_INVALID"); + } + const headers = { "content-type": "application/json" }; + if (config.route_profile.protocol_version === "gemini-native-v1beta") { + const parts = [{ text: prompt }]; + if (reference) parts.push({ inlineData: { data: reference.bytes.toString("base64"), mimeType: reference.mime_type } }); + return { + body: { + contents: [{ parts, role: "user" }], + generationConfig: { imageConfig: { aspectRatio: ratio }, responseModalities: ["TEXT", "IMAGE"] }, + }, + headers, + method: "POST", + url: config.route_profile.endpoint, + }; + } + const body = { + model: config.model_id, + prompt, + response_format: "b64_json", + size: openAiSizes[ratio], + }; + if (reference) body.image = `data:${reference.mime_type};base64,${reference.bytes.toString("base64")}`; + return { body, headers, method: "POST", url: config.route_profile.endpoint }; +} + +function pngDimensions(bytes) { + const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + if (bytes.length < 24 || !bytes.subarray(0, 8).equals(signature)) return undefined; + return { height: bytes.readUInt32BE(20), width: bytes.readUInt32BE(16) }; +} + +function jpegDimensions(bytes) { + if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return undefined; + let offset = 2; + while (offset + 9 < bytes.length) { + if (bytes[offset] !== 0xff) { offset += 1; continue; } + const marker = bytes[offset + 1]; + if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) { + return { height: bytes.readUInt16BE(offset + 5), width: bytes.readUInt16BE(offset + 7) }; + } + if (marker === 0xd8 || marker === 0xd9 || (marker >= 0xd0 && marker <= 0xd7)) { offset += 2; continue; } + const length = bytes.readUInt16BE(offset + 2); + if (length < 2) return undefined; + offset += length + 2; + } + return undefined; +} + +function webpDimensions(bytes) { + if (bytes.length < 30 || bytes.toString("ascii", 0, 4) !== "RIFF" || bytes.toString("ascii", 8, 12) !== "WEBP") return undefined; + const kind = bytes.toString("ascii", 12, 16); + if (kind === "VP8X") { + return { + height: 1 + bytes.readUIntLE(27, 3), + width: 1 + bytes.readUIntLE(24, 3), + }; + } + if (kind === "VP8 " && bytes.length >= 30) return { height: bytes.readUInt16LE(28) & 0x3fff, width: bytes.readUInt16LE(26) & 0x3fff }; + if (kind === "VP8L" && bytes.length >= 25) { + const bits = bytes.readUInt32LE(21); + return { height: 1 + ((bits >> 14) & 0x3fff), width: 1 + (bits & 0x3fff) }; + } + return undefined; +} + +function inspectImage(bytes, declaredMime) { + const png = pngDimensions(bytes); + if (png && declaredMime === "image/png") return { ...png, mime: declaredMime }; + const jpeg = jpegDimensions(bytes); + if (jpeg && declaredMime === "image/jpeg") return { ...jpeg, mime: declaredMime }; + const webp = webpDimensions(bytes); + if (webp && declaredMime === "image/webp") return { ...webp, mime: declaredMime }; + throw new Error("WP7_02_RESPONSE_MEDIA_INVALID"); +} + +function integerOrZero(value) { + return Number.isSafeInteger(value) && value >= 0 ? value : 0; +} + +function geminiUsage(response) { + const usage = response?.usageMetadata; + return { + input_units: integerOrZero(usage?.promptTokenCount), + output_units: integerOrZero(usage?.candidatesTokenCount), + total_units: integerOrZero(usage?.totalTokenCount), + }; +} + +function openAiUsage(response) { + const usage = response?.usage; + return { + input_units: integerOrZero(usage?.input_tokens ?? usage?.inputTokens), + output_units: integerOrZero(usage?.output_tokens ?? usage?.outputTokens), + total_units: integerOrZero(usage?.total_tokens ?? usage?.totalTokens), + }; +} + +export function normalizeProviderResponse(modelConfig, response) { + const config = assertModelConfig(modelConfig); + let bytes; + let mime; + let usageSummary; + if (config.route_profile.protocol_version === "gemini-native-v1beta") { + const parts = response?.candidates?.flatMap((candidate) => candidate?.content?.parts ?? []) ?? []; + const images = parts.map((part) => part?.inlineData ?? part?.inline_data).filter((entry) => entry?.data); + if (images.length !== 1) throw new Error("WP7_02_RESPONSE_SINGLE_IMAGE_REQUIRED"); + mime = images[0].mimeType ?? images[0].mime_type; + bytes = Buffer.from(images[0].data, "base64"); + usageSummary = geminiUsage(response); + } else { + if (!Array.isArray(response?.data) || response.data.length !== 1 || typeof response.data[0]?.b64_json !== "string") { + throw new Error("WP7_02_RESPONSE_SINGLE_IMAGE_REQUIRED"); + } + bytes = Buffer.from(response.data[0].b64_json, "base64"); + mime = "image/png"; + usageSummary = openAiUsage(response); + } + const media = inspectImage(bytes, mime); + return { + bytes, + dimensions: { height: media.height, width: media.width }, + evidence_hash: `sha256:${sha256(bytes)}`, + mime: media.mime, + usage_summary: usageSummary, + }; +} + +export function buildSanitizedResponseEvidence(normalized) { + const evidence = { + dimensions: structuredClone(normalized.dimensions), + evidence_hash: normalized.evidence_hash, + mime: normalized.mime, + usage_summary: structuredClone(normalized.usage_summary), + }; + return validateSanitizedEvidence(evidence); +} + +function inspectEvidenceValue(value, seen = new Set()) { + if (value && typeof value === "object") { + if (seen.has(value)) throw new Error("WP7_02_EVIDENCE_CYCLE_FORBIDDEN"); + seen.add(value); + for (const [key, entry] of Object.entries(value)) { + if (key === "verified") throw new Error("WP7_02_SHARED_VERIFIED_FORBIDDEN"); + if (forbiddenEvidenceKeys.test(key)) throw new Error("WP7_02_SENSITIVE_EVIDENCE_FORBIDDEN"); + inspectEvidenceValue(entry, seen); + } + seen.delete(value); + } else if (typeof value === "string" && /[A-Za-z]:\\Users\\/i.test(value)) { + throw new Error("WP7_02_SENSITIVE_EVIDENCE_FORBIDDEN"); + } +} + +export function validateSanitizedEvidence(evidence) { + inspectEvidenceValue(evidence); + return evidence; +} + +export async function executeProviderRequest({ fetchImpl = fetch, modelConfig, prompt, ratio, reference, token, timeoutMs = 180_000 }) { + if (typeof token !== "string" || token.length < 8) throw new Error("WP7_02_CREDENTIAL_INVALID"); + const request = buildProviderRequest({ modelConfig, prompt, ratio, reference }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const startedAt = performance.now(); + try { + const response = await fetchImpl(request.url, { + body: JSON.stringify(request.body), + headers: { ...request.headers, authorization: `Bearer ${token}` }, + method: request.method, + signal: controller.signal, + }); + const durationMs = Math.round(performance.now() - startedAt); + if (!response.ok) throw new Error(`WP7_02_UPSTREAM_HTTP_${response.status}`); + const normalized = normalizeProviderResponse(modelConfig, await response.json()); + return { + duration_ms: durationMs, + http_status: response.status, + normalized, + response_evidence: buildSanitizedResponseEvidence(normalized), + }; + } catch (error) { + if (error?.name === "AbortError") throw new Error("WP7_02_UPSTREAM_TIMEOUT"); + if (error instanceof Error && /^WP7_02_[A-Z0-9_]+$/.test(error.message)) throw error; + throw new Error("WP7_02_UPSTREAM_FAILED"); + } finally { + clearTimeout(timeout); + } +} diff --git a/supervisor/Dada.Supervisor.Tests/Program.cs b/supervisor/Dada.Supervisor.Tests/Program.cs index c148984..c14b807 100644 --- a/supervisor/Dada.Supervisor.Tests/Program.cs +++ b/supervisor/Dada.Supervisor.Tests/Program.cs @@ -16,6 +16,12 @@ internal static class Program return await RunCredentialChildAsync(); } + if (args.FirstOrDefault() == "--credential-echo") + { + Console.Write(await Console.In.ReadToEndAsync()); + return 0; + } + if (args.FirstOrDefault() == "--instance-probe") { using var instance = await SingleInstanceCoordinator.TryAcquireAsync(args[1], args[2]); @@ -120,6 +126,28 @@ internal static class Program var workerProbe = await LaunchCredentialProbeAsync(ChildRole.Worker, store); EqualSequence(new[] { CredentialCatalog.WorkerAiGateway }, workerProbe.Names, "Worker credential scope"); + var leakProbe = await CredentialProcessLauncher.RunToCompletionAsync( + new ProcessStartInfo(Environment.ProcessPath!, "--credential-echo"), ChildRole.Worker, store); + True(leakProbe.SensitiveOutputDetected, "credential echo must be detected"); + Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded"); + Equal(string.Empty, leakProbe.StandardError, "credential echo error discarded"); + + var externalArguments = new[] + { + "--service", "ai-gateway-service-id", + "--model", "gpt-image-2", + "--run-id", "wp7-02-supervisor-probe", + "--candidate-record", "candidate.json", + "--config-manifest", "config.json", + "--evidence-dir", "evidence", + "--max-real-calls", "120", + "--confirm-controlled-real", + "--execute-controlled-real", + }; + EqualSequence(externalArguments, ControlledExternalValidationLauncher.ValidateArguments(externalArguments), "controlled external argument allowlist"); + Throws( + () => ControlledExternalValidationLauncher.ValidateArguments(externalArguments.Where(value => value != "--confirm-controlled-real").ToArray()), + "controlled external confirmation required"); store.Delete(CredentialCatalog.WorkerAiGateway); await ThrowsAsync( @@ -166,15 +194,10 @@ internal static class Program private static async Task LaunchCredentialProbeAsync(ChildRole role, ICredentialStore store) { - var startInfo = new ProcessStartInfo(Environment.ProcessPath!, "--credential-child") - { - RedirectStandardOutput = true, - }; - using var process = await CredentialProcessLauncher.StartAsync(startInfo, role, store); - var output = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); - Equal(0, process.ExitCode, "credential child exit code"); - return JsonSerializer.Deserialize(output, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + var result = await CredentialProcessLauncher.RunToCompletionAsync(new ProcessStartInfo(Environment.ProcessPath!, "--credential-child"), role, store); + Equal(0, result.ExitCode, "credential child exit code"); + False(result.SensitiveOutputDetected, "credential child output contains injected value"); + return JsonSerializer.Deserialize(result.StandardOutput, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("Credential child returned invalid JSON."); } @@ -355,6 +378,19 @@ internal static class Program throw new InvalidOperationException(message); } + private static void Throws(Action action, string message) where TException : Exception + { + try + { + action(); + } + catch (TException) + { + return; + } + throw new InvalidOperationException(message); + } + private sealed record CredentialProbe(string[] Names, bool EnvironmentContainsMarker, bool ArgumentsContainMarker); private sealed class TestCredentialStore : ICredentialStore diff --git a/supervisor/Dada.Supervisor/ControlledExternalValidationLauncher.cs b/supervisor/Dada.Supervisor/ControlledExternalValidationLauncher.cs new file mode 100644 index 0000000..70a1e1f --- /dev/null +++ b/supervisor/Dada.Supervisor/ControlledExternalValidationLauncher.cs @@ -0,0 +1,138 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Dada.Supervisor; + +internal static partial class ControlledExternalValidationLauncher +{ + private static readonly HashSet AllowedModels = + [ + "gemini-3.1-flash-image-preview", + "gemini-3-pro-image-preview", + "gpt-image-2", + ]; + + private static readonly HashSet ValueOptions = + [ + "--candidate-record", + "--config-manifest", + "--evidence-dir", + "--max-real-calls", + "--model", + "--run-id", + "--service", + ]; + + private static readonly HashSet SwitchOptions = + [ + "--confirm-controlled-real", + "--execute-controlled-real", + ]; + + internal static async Task 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(StringComparer.Ordinal); + var switches = new HashSet(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" + || !values.ContainsKey("--candidate-record") + || !values.ContainsKey("--config-manifest") + || !values.ContainsKey("--evidence-dir") + || !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(); +} diff --git a/supervisor/Dada.Supervisor/Credentials.cs b/supervisor/Dada.Supervisor/Credentials.cs index 8bc8526..9003137 100644 --- a/supervisor/Dada.Supervisor/Credentials.cs +++ b/supervisor/Dada.Supervisor/Credentials.cs @@ -37,8 +37,58 @@ internal interface ICredentialStore internal sealed class MissingCredentialException(string target) : InvalidOperationException($"Required credential is not configured: {target}"); +internal sealed record CredentialProcessResult(int ExitCode, string StandardOutput, string StandardError, bool SensitiveOutputDetected); + internal static class CredentialProcessLauncher { + internal static async Task RunToCompletionAsync( + ProcessStartInfo startInfo, + ChildRole role, + ICredentialStore store, + CancellationToken cancellationToken = default) + { + var credentials = new Dictionary(StringComparer.Ordinal); + foreach (var target in CredentialCatalog.RequiredFor(role)) + { + credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target); + } + + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + startInfo.RedirectStandardInput = true; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start credential child process."); + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + var payload = JsonSerializer.SerializeToUtf8Bytes(credentials); + try + { + await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken); + await process.StandardInput.BaseStream.FlushAsync(cancellationToken); + process.StandardInput.Close(); + await process.WaitForExitAsync(cancellationToken); + var output = await outputTask; + var error = await errorTask; + var sensitive = credentials.Values.Where(value => value.Length > 0).Any(value => + output.Contains(value, StringComparison.Ordinal) || error.Contains(value, StringComparison.Ordinal)); + return sensitive + ? new CredentialProcessResult(1, string.Empty, string.Empty, true) + : new CredentialProcessResult(process.ExitCode, output, error, false); + } + catch + { + if (!process.HasExited) process.Kill(entireProcessTree: true); + throw; + } + finally + { + Array.Clear(payload); + foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty; + credentials.Clear(); + } + } + internal static async Task StartAsync( ProcessStartInfo startInfo, ChildRole role, diff --git a/supervisor/Dada.Supervisor/OfflineCommandRouter.cs b/supervisor/Dada.Supervisor/OfflineCommandRouter.cs index 05700b5..2e0a32f 100644 --- a/supervisor/Dada.Supervisor/OfflineCommandRouter.cs +++ b/supervisor/Dada.Supervisor/OfflineCommandRouter.cs @@ -29,6 +29,7 @@ internal static class OfflineCommandRouter "secrets" => RunSecrets(args.Skip(1).ToArray(), credentials), "admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials), "doctor" when args.Length == 1 => RunDoctor(credentials), + "validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials), _ => Usage(), }; } @@ -198,7 +199,7 @@ internal static class OfflineCommandRouter private static int Usage() { - WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear; admin-allowlist add|remove|status; doctor", false); + WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear; admin-allowlist add|remove|status; doctor; validate-external", false); return 2; } } diff --git a/tests/package/wp7-02-controlled-executor.test.mjs b/tests/package/wp7-02-controlled-executor.test.mjs index 720578d..e2d9bba 100644 --- a/tests/package/wp7-02-controlled-executor.test.mjs +++ b/tests/package/wp7-02-controlled-executor.test.mjs @@ -6,6 +6,7 @@ import { buildControlledExecutionPlan, buildProviderRequest, buildSanitizedResponseEvidence, + executeProviderRequest, normalizeProviderResponse, validateSanitizedEvidence, } from "../../scripts/lib/wp7-02-controlled-executor.mjs"; @@ -107,3 +108,33 @@ test("TDD-WP7-EXT-001 rejects sensitive or shared evidence fields", () => { } assert.throws(() => validateSanitizedEvidence({ status: "passed", verified: true }), /WP7_02_SHARED_VERIFIED_FORBIDDEN/); }); + +test("TDD-WP7-EXT-001 confines the credential to the request header and discards provider error bodies", async () => { + const credentialMarker = "controlled-secret-value-for-test-only"; + const success = await executeProviderRequest({ + fetchImpl: async (_url, init) => { + assert.equal(init.headers.authorization, `Bearer ${credentialMarker}`); + return new Response(JSON.stringify({ + candidates: [{ content: { parts: [{ inlineData: { data: onePixelPng.toString("base64"), mimeType: "image/png" } }] } }], + }), { headers: { "content-type": "application/json" }, status: 200 }); + }, + modelConfig: models[0], + prompt: "controlled fixture prompt", + ratio: "1:1", + token: credentialMarker, + }); + assert.equal(success.http_status, 200); + assert.doesNotMatch(JSON.stringify({ ...success, normalized: undefined }), new RegExp(credentialMarker)); + + await assert.rejects(() => executeProviderRequest({ + fetchImpl: async () => new Response(JSON.stringify({ provider_body: credentialMarker }), { status: 502 }), + modelConfig: models[0], + prompt: "controlled fixture prompt", + ratio: "1:1", + token: credentialMarker, + }), (error) => { + assert.equal(error.message, "WP7_02_UPSTREAM_HTTP_502"); + assert.doesNotMatch(error.message, new RegExp(credentialMarker)); + return true; + }); +});