summaryrefslogtreecommitdiff
path: root/src/Agent.Core/Tools/InternalToolValidator.cs
blob: 091b790d6f257dd4b0e79777df03e668d07e2986 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
using System.Text.Json;

namespace Agent.Core.Tools;

public static class InternalToolValidator
{
    public static void Validate(string name, JsonElement arguments)
    {
        InternalToolDefinition definition = InternalToolCatalog.Get(name);
        if (arguments.ValueKind != JsonValueKind.Object) throw new ToolValidationException("Tool arguments must be an object.");
        JsonElement properties = definition.Parameters.GetProperty("properties");
        var known = properties.EnumerateObject().Select(static item => item.Name).ToHashSet(StringComparer.Ordinal);
        foreach (JsonProperty argument in arguments.EnumerateObject())
        {
            if (!known.Contains(argument.Name)) throw new ToolValidationException($"Unknown argument '{argument.Name}'.");
            ValidateValue(argument.Name, argument.Value, properties.GetProperty(argument.Name));
        }
        foreach (JsonElement required in definition.Parameters.GetProperty("required").EnumerateArray())
        {
            string requiredName = required.GetString()!;
            if (!arguments.TryGetProperty(requiredName, out _)) throw new ToolValidationException($"Missing required argument '{requiredName}'.");
        }
        ValidateOperationSpecific(name, arguments);
    }

    private static void ValidateValue(string name, JsonElement value, JsonElement schema)
    {
        if (value.ValueKind == JsonValueKind.Null && AllowsNull(schema)) return;
        bool validType = GetTypes(schema).Any(type => type switch
        {
            "string" => value.ValueKind == JsonValueKind.String,
            "integer" => value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out _),
            "array" => value.ValueKind == JsonValueKind.Array,
            _ => false
        });
        if (!validType) throw new ToolValidationException($"Argument '{name}' has an invalid type.");
        if (schema.TryGetProperty("enum", out JsonElement enumValues) && !enumValues.EnumerateArray().Any(item => JsonElement.DeepEquals(item, value)))
            throw new ToolValidationException($"Argument '{name}' has an unsupported value.");
        if (value.ValueKind == JsonValueKind.Number)
        {
            long number = value.GetInt64();
            if (schema.TryGetProperty("minimum", out JsonElement minimum) && number < minimum.GetInt64()) throw new ToolValidationException($"Argument '{name}' is below its minimum.");
            if (schema.TryGetProperty("maximum", out JsonElement maximum) && number > maximum.GetInt64()) throw new ToolValidationException($"Argument '{name}' exceeds its maximum.");
        }
        if (value.ValueKind == JsonValueKind.Array && schema.TryGetProperty("items", out JsonElement items))
            foreach (JsonElement item in value.EnumerateArray()) ValidateValue(name, item, items);
    }

    private static IEnumerable<string> GetTypes(JsonElement schema)
    {
        JsonElement type = schema.GetProperty("type");
        return type.ValueKind == JsonValueKind.Array
            ? type.EnumerateArray().Select(static item => item.GetString()!).ToArray()
            : [type.GetString()!];
    }
    private static bool AllowsNull(JsonElement schema) => GetTypes(schema).Contains("null", StringComparer.Ordinal);

    private static void ValidateOperationSpecific(string name, JsonElement arguments)
    {
        if (name == "file_read" && arguments.GetProperty("mode").GetString() == "range" && !arguments.TryGetProperty("length", out _))
            throw new ToolValidationException("Range reads require length.");
        if (name == "file_edit")
        {
            string operation = arguments.GetProperty("operation").GetString()!;
            if (operation is "overwrite" or "atomic_replace" && (!arguments.TryGetProperty("content", out JsonElement content) || content.ValueKind != JsonValueKind.String))
                throw new ToolValidationException($"{operation} requires content.");
            if (operation == "patch" && (!arguments.TryGetProperty("patch", out JsonElement patch) || patch.ValueKind != JsonValueKind.String))
                throw new ToolValidationException("patch requires patch text.");
        }
        if (name == "task_update")
        {
            string operation = arguments.GetProperty("operation").GetString()!;
            if (operation == "add") Require(arguments, "description", "budget_class", "optimistic_tokens", "most_likely_tokens", "pessimistic_tokens");
            if (operation == "add_dependency") Require(arguments, "task_id", "depends_on");
            if (operation == "set_state") Require(arguments, "task_id", "state");
            if (operation == "set_estimate") Require(arguments, "task_id", "budget_class", "optimistic_tokens", "most_likely_tokens", "pessimistic_tokens");
        }
        // dir: path and operation are already required by the schema; no operation-specific
        // additional arguments are needed since mkdir/rmdir/list only use the path field.

        static void Require(JsonElement value, params string[] properties)
        {
            foreach (string property in properties)
                if (!value.TryGetProperty(property, out JsonElement item) || item.ValueKind == JsonValueKind.Null)
                    throw new ToolValidationException($"{property} is required for this operation.");
        }
    }
}