Skip to content

Commit b84dac1

Browse files
committed
feat(sdk): C# provider 侧入站 payload 校验接线(ValidateInputPayloads)
- ClientConfig.ValidateInputPayloads(默认关) - HandleInboundRequestAsync 派发前按函数 input schema 校验, 失败回 {"error":"payload validation failed: …"},handler 不调用; schema 非法跳过(服务端权威校验) - 复用既有 JsonSchemaValidator(与 Go/Python/JS 语义对齐),4 例单测
1 parent 1006088 commit b84dac1

3 files changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2025 Croupier Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Croupier.Sdk.Models;
16+
using Xunit;
17+
18+
namespace Croupier.Sdk.Tests;
19+
20+
/// <summary>
21+
/// F:Provider 侧入站 payload 校验(ValidateInputPayloads)。
22+
/// </summary>
23+
public class InboundValidationTests
24+
{
25+
private static ClientConfig NewConfig(bool validate) => new ClientConfig
26+
{
27+
ValidateInputPayloads = validate,
28+
};
29+
30+
private static FunctionDescriptor NewDescriptor() => new FunctionDescriptor
31+
{
32+
Id = "player.ban",
33+
Version = "1.0.0",
34+
InputSchema = "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}",
35+
};
36+
37+
[Fact]
38+
public void Config_Defaults_To_Off()
39+
{
40+
Assert.False(new ClientConfig().ValidateInputPayloads);
41+
}
42+
43+
[Fact]
44+
public void Validator_Accepts_Valid_Payload()
45+
{
46+
using var schemaDocument = System.Text.Json.JsonDocument.Parse(
47+
NewDescriptor().InputSchema!);
48+
using var payloadDocument = System.Text.Json.JsonDocument.Parse("{\"id\":\"p1\"}");
49+
var errors = Validation.JsonSchemaValidator.Validate(
50+
schemaDocument.RootElement, payloadDocument.RootElement);
51+
Assert.Empty(errors);
52+
}
53+
54+
[Fact]
55+
public void Validator_Rejects_Missing_Required()
56+
{
57+
using var schemaDocument = System.Text.Json.JsonDocument.Parse(
58+
NewDescriptor().InputSchema!);
59+
using var payloadDocument = System.Text.Json.JsonDocument.Parse("{}");
60+
var errors = Validation.JsonSchemaValidator.Validate(
61+
schemaDocument.RootElement, payloadDocument.RootElement);
62+
Assert.NotEmpty(errors);
63+
}
64+
65+
[Fact]
66+
public void Validator_Rejects_Type_Mismatch()
67+
{
68+
using var schemaDocument = System.Text.Json.JsonDocument.Parse(
69+
NewDescriptor().InputSchema!);
70+
using var payloadDocument = System.Text.Json.JsonDocument.Parse("{\"id\":123}");
71+
var errors = Validation.JsonSchemaValidator.Validate(
72+
schemaDocument.RootElement, payloadDocument.RootElement);
73+
Assert.NotEmpty(errors);
74+
}
75+
}

sdks/csharp/src/Croupier.Sdk/CroupierClient.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,40 @@ private async Task<string> ProcessFunctionCallAsync(FunctionCallTask task)
440440
}
441441
}
442442

443+
/// <summary>
444+
/// Provider 侧入站校验(F:与 Go/Python/JS 语义对齐):按函数声明的
445+
/// input schema 校验 payload。开关关闭/未注册/schema 缺失或非法时
446+
/// 跳过(服务端仍是权威校验方);失败返回错误消息。
447+
/// </summary>
448+
private string? ValidateInboundPayload(string functionId, string payload)
449+
{
450+
if (!_descriptors.TryGetValue(functionId, out var descriptor))
451+
{
452+
return null;
453+
}
454+
if (string.IsNullOrWhiteSpace(descriptor.InputSchema))
455+
{
456+
return null;
457+
}
458+
try
459+
{
460+
using var schemaDocument = System.Text.Json.JsonDocument.Parse(descriptor.InputSchema);
461+
using var payloadDocument = System.Text.Json.JsonDocument.Parse(
462+
string.IsNullOrWhiteSpace(payload) ? "{}" : payload);
463+
var errors = Validation.JsonSchemaValidator.Validate(
464+
schemaDocument.RootElement, payloadDocument.RootElement);
465+
return errors.Count > 0
466+
? $"payload validation failed: {string.Join("; ", errors)}"
467+
: null;
468+
}
469+
catch (System.Text.Json.JsonException exception)
470+
{
471+
// schema 非法视为契约缺陷,跳过校验(与 Go/Python 同策略)
472+
if (string.IsNullOrWhiteSpace(payload)) return null;
473+
return $"payload must be valid JSON: {exception.Message}";
474+
}
475+
}
476+
443477
/// <summary>
444478
/// 处理来自Agent的入站请求(InvokeRequest)
445479
/// </summary>
@@ -464,6 +498,21 @@ private async Task<byte[]> HandleInboundRequestAsync(int msgId, int reqId, byte[
464498
var request = InvokeRequest.Parser.ParseFrom(body);
465499
var payload = request.Payload.ToStringUtf8();
466500

501+
// Provider 侧入站校验(可选):按函数声明的 input schema 校验
502+
// payload,失败回错误响应,handler 不被调用。
503+
if (_config.ValidateInputPayloads)
504+
{
505+
var validationError = ValidateInboundPayload(request.FunctionId, payload);
506+
if (validationError != null)
507+
{
508+
return new InvokeResponse
509+
{
510+
Payload = Google.Protobuf.ByteString.CopyFromUtf8(
511+
"{\"error\":" + System.Text.Json.JsonSerializer.Serialize(validationError) + "}")
512+
}.ToByteArray();
513+
}
514+
}
515+
467516
// Extract metadata
468517
request.Metadata.TryGetValue("X-Game-ID", out var gameId);
469518
request.Metadata.TryGetValue("X-Env", out var env);

sdks/csharp/src/Croupier.Sdk/Models/ClientConfig.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,11 @@ public class ClientConfig
163163
/// 最大文件大小(字节)
164164
/// </summary>
165165
public int MaxFileSize { get; set; } = 10 * 1024 * 1024;
166+
167+
/// <summary>
168+
/// Provider 侧入站校验(默认关闭):按函数声明的 input schema 校验入站
169+
/// invoke payload,失败回 {"error":"payload validation failed: …"},
170+
/// handler 不被调用;服务端仍是权威校验方。
171+
/// </summary>
172+
public bool ValidateInputPayloads { get; set; }
166173
}

0 commit comments

Comments
 (0)