feat(ai): enforce local-first routing
Keep external providers behind server consent, task, and prompt-cost gates while persisting actual provider provenance.
This commit is contained in:
@@ -46,6 +46,20 @@ public sealed class AiPrivacyPolicyTests
|
||||
Assert.Equal("local", decision.Provider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("local_only")]
|
||||
[InlineData("unexpected")]
|
||||
public async Task Local_only_or_invalid_admin_mode_disables_external_processing(string routingMode)
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync(adminEnabled: true, routingMode: routingMode);
|
||||
await fixture.CreateUserAsync(externalAllowed: true, pro: true);
|
||||
|
||||
var decision = await fixture.Policy.EvaluateAsync("user-1");
|
||||
|
||||
Assert.False(decision.ExternalProcessingAllowed);
|
||||
Assert.Equal("local", decision.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cv_request_carries_external_permission_only_after_the_policy_allows_it()
|
||||
{
|
||||
@@ -57,7 +71,10 @@ public sealed class AiPrivacyPolicyTests
|
||||
new[] { new Claim(ClaimTypes.NameIdentifier, "user-1") }, "local")),
|
||||
};
|
||||
var capture = new CaptureHandler();
|
||||
var handler = new AiPrivacyHeaderHandler(new HttpContextAccessor { HttpContext = context }, fixture.Policy)
|
||||
var handler = new AiPrivacyHeaderHandler(
|
||||
new HttpContextAccessor { HttpContext = context },
|
||||
fixture.Policy,
|
||||
new AiOperationExecutionScope())
|
||||
{
|
||||
InnerHandler = capture,
|
||||
};
|
||||
@@ -68,15 +85,40 @@ public sealed class AiPrivacyPolicyTests
|
||||
Assert.Equal("true", capture.ExternalAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Background_operation_carries_its_rechecked_policy_and_task_without_http_user_context()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync(adminEnabled: true);
|
||||
var executionScope = new AiOperationExecutionScope();
|
||||
var capture = new CaptureHandler();
|
||||
var handler = new AiPrivacyHeaderHandler(new HttpContextAccessor(), fixture.Policy, executionScope)
|
||||
{
|
||||
InnerHandler = capture,
|
||||
};
|
||||
var lease = new UserOperationLease(Guid.NewGuid(), "user-1", "lease", "strategy.snapshot",
|
||||
"external_allowed", "job", "42", 1, DateTime.UtcNow.AddMinutes(5));
|
||||
|
||||
using var routing = executionScope.Use(new AiOperationExecutionContext(lease, "external_allowed"));
|
||||
using var client = new HttpClient(handler);
|
||||
await client.PostAsync("http://ai-service/cv/rewrite", new StringContent("{}"));
|
||||
|
||||
Assert.Equal("true", capture.ExternalAllowed);
|
||||
Assert.Equal("strategy.snapshot", capture.TaskType);
|
||||
}
|
||||
|
||||
private sealed class CaptureHandler : HttpMessageHandler
|
||||
{
|
||||
public string? ExternalAllowed { get; private set; }
|
||||
public string? TaskType { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
ExternalAllowed = request.Headers.TryGetValues(AiPrivacyPolicy.ExternalAllowedHeader, out var values)
|
||||
? values.Single()
|
||||
: null;
|
||||
TaskType = request.Headers.TryGetValues(AiPrivacyPolicy.TaskTypeHeader, out var taskValues)
|
||||
? taskValues.Single()
|
||||
: null;
|
||||
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK));
|
||||
}
|
||||
}
|
||||
@@ -94,7 +136,7 @@ public sealed class AiPrivacyPolicyTests
|
||||
Policy = policy;
|
||||
}
|
||||
|
||||
public static async Task<Fixture> CreateAsync(bool adminEnabled)
|
||||
public static async Task<Fixture> CreateAsync(bool adminEnabled, string routingMode = "local_first")
|
||||
{
|
||||
var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
@@ -102,6 +144,7 @@ public sealed class AiPrivacyPolicyTests
|
||||
{
|
||||
["Ai:ExternalProcessingEnabled"] = adminEnabled.ToString(),
|
||||
["Ai:ExternalProvider"] = "gemini",
|
||||
["Ai:RoutingMode"] = routingMode,
|
||||
}).Build();
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
Reference in New Issue
Block a user