agent-client-protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +190 -0
- data/README.md +126 -0
- data/lib/agent_client_protocol/codec.rb +111 -0
- data/lib/agent_client_protocol/constants.rb +78 -0
- data/lib/agent_client_protocol/decoder.rb +134 -0
- data/lib/agent_client_protocol/error.rb +104 -0
- data/lib/agent_client_protocol/methods.rb +19 -0
- data/lib/agent_client_protocol/protocol_version.rb +56 -0
- data/lib/agent_client_protocol/rpc.rb +160 -0
- data/lib/agent_client_protocol/schema_registry.rb +112 -0
- data/lib/agent_client_protocol/type_registry.rb +99 -0
- data/lib/agent_client_protocol/types/base.rb +314 -0
- data/lib/agent_client_protocol/types.rb +8 -0
- data/lib/agent_client_protocol/validator.rb +300 -0
- data/lib/agent_client_protocol/version.rb +5 -0
- data/lib/agent_client_protocol.rb +41 -0
- data/schema/meta.json +24 -0
- data/schema/meta.unstable.json +31 -0
- data/schema/schema.json +3430 -0
- data/schema/schema.unstable.json +4125 -0
- metadata +64 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module AgentClientProtocol
|
|
6
|
+
module Types
|
|
7
|
+
class Base
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :definition_name, :schema, :properties, :required_keys, :ruby_property_map, :ruby_to_json_property_map,
|
|
10
|
+
:additional_properties, :property_schemas, :unstable
|
|
11
|
+
|
|
12
|
+
def configure(definition_name:, schema:, unstable: false)
|
|
13
|
+
@definition_name = definition_name
|
|
14
|
+
@schema = schema
|
|
15
|
+
@unstable = unstable
|
|
16
|
+
|
|
17
|
+
@properties = (schema["properties"] || {}).keys.freeze
|
|
18
|
+
@property_schemas = (schema["properties"] || {}).freeze
|
|
19
|
+
@required_keys = (schema["required"] || []).freeze
|
|
20
|
+
@additional_properties = schema["additionalProperties"]
|
|
21
|
+
@ruby_property_map = build_ruby_property_map(@properties)
|
|
22
|
+
@ruby_to_json_property_map = @ruby_property_map.each_with_object({}) do |(json_key, ruby_key), acc|
|
|
23
|
+
acc[ruby_key.to_s] = json_key
|
|
24
|
+
end.freeze
|
|
25
|
+
|
|
26
|
+
define_property_readers!
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def coerce(value)
|
|
31
|
+
return value if value.is_a?(self)
|
|
32
|
+
|
|
33
|
+
new(value)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build(attributes = nil, **kwargs)
|
|
37
|
+
if attributes && !kwargs.empty?
|
|
38
|
+
raise ArgumentError, "pass either attributes Hash or keyword args, not both"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
payload = attributes || kwargs
|
|
42
|
+
new(payload)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_ruby_property_map(property_names)
|
|
48
|
+
reserved = instance_methods.map(&:to_s).to_set
|
|
49
|
+
mapping = {}
|
|
50
|
+
|
|
51
|
+
property_names.each do |json_key|
|
|
52
|
+
candidate = ruby_identifier_for(json_key)
|
|
53
|
+
candidate = "field" if candidate.empty?
|
|
54
|
+
candidate = "_#{candidate}" if candidate.match?(/\A\d/)
|
|
55
|
+
|
|
56
|
+
while reserved.include?(candidate)
|
|
57
|
+
candidate = "#{candidate}_field"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
reserved.add(candidate)
|
|
61
|
+
mapping[json_key] = candidate.to_sym
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
mapping.freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ruby_identifier_for(json_key)
|
|
68
|
+
sanitized = json_key.to_s.sub(/\A_+/, "")
|
|
69
|
+
step_one = sanitized.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
70
|
+
step_two = step_one.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
71
|
+
step_two.tr("-", "_").downcase
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def define_property_readers!
|
|
75
|
+
@ruby_property_map.each do |json_key, ruby_key|
|
|
76
|
+
next if method_defined?(ruby_key)
|
|
77
|
+
|
|
78
|
+
define_method(ruby_key) do
|
|
79
|
+
@attributes[json_key]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def initialize(attributes = nil)
|
|
86
|
+
unless attributes.nil? || attributes.is_a?(Hash)
|
|
87
|
+
raise ArgumentError, "#{self.class.definition_name} expects a Hash payload"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
attributes ||= {}
|
|
91
|
+
normalized = normalize_attributes(attributes)
|
|
92
|
+
|
|
93
|
+
missing = self.class.required_keys.reject { |key| normalized.key?(key) }
|
|
94
|
+
unless missing.empty?
|
|
95
|
+
raise ::AgentClientProtocol::Error.invalid_params(
|
|
96
|
+
"#{self.class.definition_name} missing required keys: #{missing.join(', ')}"
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if self.class.additional_properties == false
|
|
101
|
+
unknown_keys = normalized.keys - self.class.properties
|
|
102
|
+
unless unknown_keys.empty?
|
|
103
|
+
raise ::AgentClientProtocol::Error.invalid_params(
|
|
104
|
+
"#{self.class.definition_name} unknown keys: #{unknown_keys.join(', ')}"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@attributes = normalized.freeze
|
|
110
|
+
freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def [](key)
|
|
114
|
+
json_key = self.class.ruby_to_json_property_map[key.to_s] || key.to_s
|
|
115
|
+
@attributes[json_key]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def fetch(key, *args)
|
|
119
|
+
json_key = self.class.ruby_to_json_property_map[key.to_s] || key.to_s
|
|
120
|
+
@attributes.fetch(json_key, *args)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def key?(key)
|
|
124
|
+
json_key = self.class.ruby_to_json_property_map[key.to_s] || key.to_s
|
|
125
|
+
@attributes.key?(json_key)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def to_h
|
|
129
|
+
deep_to_h(@attributes)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def to_json(*args)
|
|
133
|
+
to_h.to_json(*args)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ==(other)
|
|
137
|
+
other.is_a?(self.class) && other.to_h == to_h
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
alias eql? ==
|
|
141
|
+
|
|
142
|
+
def hash
|
|
143
|
+
[self.class, @attributes].hash
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def normalize_attributes(attributes)
|
|
149
|
+
attributes.each_with_object({}) do |(k, v), acc|
|
|
150
|
+
key = normalize_input_key(k)
|
|
151
|
+
schema = self.class.property_schemas[key]
|
|
152
|
+
acc[key] = normalize_input_value(v, schema)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def normalize_input_key(key)
|
|
157
|
+
key_str = key.to_s
|
|
158
|
+
return key_str if self.class.properties.include?(key_str)
|
|
159
|
+
|
|
160
|
+
self.class.ruby_to_json_property_map[key_str] || key_str
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def normalize_input_value(value, schema = nil)
|
|
164
|
+
return self.class.coerce_for_schema(value, schema, unstable: self.class.unstable) unless schema.nil?
|
|
165
|
+
|
|
166
|
+
if value.is_a?(Base) || value.is_a?(Scalar)
|
|
167
|
+
value
|
|
168
|
+
elsif value.is_a?(Hash)
|
|
169
|
+
value.transform_keys(&:to_s).transform_values { |nested| normalize_input_value(nested) }
|
|
170
|
+
elsif value.is_a?(Array)
|
|
171
|
+
value.map { |item| normalize_input_value(item) }
|
|
172
|
+
else
|
|
173
|
+
value
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def deep_to_h(value)
|
|
178
|
+
case value
|
|
179
|
+
when Base
|
|
180
|
+
value.to_h
|
|
181
|
+
when Scalar
|
|
182
|
+
value.to_h
|
|
183
|
+
when Hash
|
|
184
|
+
value.each_with_object({}) do |(k, v), acc|
|
|
185
|
+
acc[k] = deep_to_h(v)
|
|
186
|
+
end
|
|
187
|
+
when Array
|
|
188
|
+
value.map { |item| deep_to_h(item) }
|
|
189
|
+
else
|
|
190
|
+
value
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class << self
|
|
195
|
+
def coerce_for_schema(value, schema, unstable:)
|
|
196
|
+
return value if schema.nil?
|
|
197
|
+
|
|
198
|
+
if schema["$ref"]
|
|
199
|
+
return coerce_ref(value, schema["$ref"], unstable: unstable)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if schema["type"] == "array" && value.is_a?(Array)
|
|
203
|
+
item_schema = schema["items"]
|
|
204
|
+
return value.map { |item| coerce_for_schema(item, item_schema, unstable: unstable) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if schema["type"] == "object" && value.is_a?(Hash)
|
|
208
|
+
inline_properties = schema["properties"] || {}
|
|
209
|
+
normalized = value.transform_keys(&:to_s)
|
|
210
|
+
return normalized.each_with_object({}) do |(k, v), acc|
|
|
211
|
+
acc[k] = coerce_for_schema(v, inline_properties[k], unstable: unstable)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if schema["anyOf"].is_a?(Array)
|
|
216
|
+
return coerce_union(value, schema["anyOf"], unstable: unstable)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if schema["oneOf"].is_a?(Array)
|
|
220
|
+
return coerce_union(value, schema["oneOf"], unstable: unstable)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
if schema["allOf"].is_a?(Array)
|
|
224
|
+
return schema["allOf"].reduce(value) do |memo, sub_schema|
|
|
225
|
+
coerce_for_schema(memo, sub_schema, unstable: unstable)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
value
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
def coerce_ref(value, ref, unstable:)
|
|
235
|
+
definition_name = parse_ref_definition(ref)
|
|
236
|
+
return value if definition_name.nil?
|
|
237
|
+
|
|
238
|
+
klass = ::AgentClientProtocol::TypeRegistry.fetch(definition_name, unstable: unstable)
|
|
239
|
+
return value if klass.nil?
|
|
240
|
+
|
|
241
|
+
klass.coerce(value)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def coerce_union(value, schemas, unstable:)
|
|
245
|
+
return nil if value.nil? && schemas.any? { |s| s["type"] == "null" }
|
|
246
|
+
|
|
247
|
+
schemas.each do |sub_schema|
|
|
248
|
+
next if sub_schema["type"] == "null"
|
|
249
|
+
|
|
250
|
+
coerced = coerce_for_schema(value, sub_schema, unstable: unstable)
|
|
251
|
+
return coerced unless coerced.equal?(value)
|
|
252
|
+
rescue StandardError
|
|
253
|
+
next
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
value
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def parse_ref_definition(ref)
|
|
260
|
+
match = ref.match(%r{\A#/\$defs/([^/]+)\z})
|
|
261
|
+
return nil if match.nil?
|
|
262
|
+
|
|
263
|
+
match[1]
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
class Scalar
|
|
269
|
+
class << self
|
|
270
|
+
attr_reader :definition_name, :schema, :unstable, :coercer
|
|
271
|
+
|
|
272
|
+
def configure(definition_name:, schema:, unstable: false, coercer: nil)
|
|
273
|
+
@definition_name = definition_name
|
|
274
|
+
@schema = schema
|
|
275
|
+
@unstable = unstable
|
|
276
|
+
@coercer = coercer
|
|
277
|
+
freeze
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def coerce(value)
|
|
281
|
+
return value if value.is_a?(self)
|
|
282
|
+
|
|
283
|
+
normalized = coercer.nil? ? value : coercer.call(value)
|
|
284
|
+
new(normalized)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
attr_reader :value
|
|
289
|
+
|
|
290
|
+
def initialize(value)
|
|
291
|
+
@value = value
|
|
292
|
+
freeze
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def to_h
|
|
296
|
+
value
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def to_json(*args)
|
|
300
|
+
value.to_json(*args)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def ==(other)
|
|
304
|
+
other.is_a?(self.class) && other.value == value
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
alias eql? ==
|
|
308
|
+
|
|
309
|
+
def hash
|
|
310
|
+
[self.class, value].hash
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentClientProtocol
|
|
4
|
+
class Validator
|
|
5
|
+
class ValidationError < StandardError
|
|
6
|
+
attr_reader :path
|
|
7
|
+
|
|
8
|
+
def initialize(path, message)
|
|
9
|
+
@path = path
|
|
10
|
+
super(message)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def validate(definition_name, payload, unstable: false)
|
|
16
|
+
definitions = SchemaRegistry.defs(unstable: unstable)
|
|
17
|
+
definition_key = definition_name.to_s
|
|
18
|
+
schema = definitions.fetch(definition_key) do
|
|
19
|
+
raise ValidationError.new("$", "unknown schema definition #{definition_key}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
validate_schema(schema, payload, definitions, "$")
|
|
23
|
+
true
|
|
24
|
+
rescue ValidationError => e
|
|
25
|
+
raise Error.invalid_params("invalid payload for #{definition_name} at #{e.path}: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def validate_schema(schema, value, definitions, path)
|
|
31
|
+
return if schema.nil? || schema == true
|
|
32
|
+
raise ValidationError.new(path, "value is not allowed") if schema == false
|
|
33
|
+
raise ValidationError.new(path, "invalid schema node") unless schema.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
if (ref = schema["$ref"])
|
|
36
|
+
validate_schema(resolve_ref(ref, definitions, path), value, definitions, path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return if protocol_version_legacy_string?(schema, value, definitions)
|
|
40
|
+
|
|
41
|
+
validate_type(schema["type"], value, path) if schema.key?("type")
|
|
42
|
+
validate_format(schema["format"], value, path) if schema.key?("format")
|
|
43
|
+
validate_enum(schema["enum"], value, path) if schema.key?("enum")
|
|
44
|
+
validate_const(schema["const"], value, path) if schema.key?("const")
|
|
45
|
+
validate_numeric_bounds(schema, value, path)
|
|
46
|
+
validate_string_bounds(schema, value, path)
|
|
47
|
+
validate_array_bounds(schema, value, path)
|
|
48
|
+
|
|
49
|
+
validate_object(schema, value, definitions, path)
|
|
50
|
+
validate_items(schema["items"], value, definitions, path) if schema.key?("items")
|
|
51
|
+
|
|
52
|
+
validate_all_of(schema["allOf"], value, definitions, path) if schema["allOf"].is_a?(Array)
|
|
53
|
+
validate_any_of(schema["anyOf"], value, definitions, path) if schema["anyOf"].is_a?(Array)
|
|
54
|
+
validate_one_of(schema["oneOf"], value, definitions, path) if schema["oneOf"].is_a?(Array)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_ref(ref, definitions, path)
|
|
58
|
+
match = ref.to_s.match(%r{\A#/\$defs/([^/]+)\z})
|
|
59
|
+
raise ValidationError.new(path, "unsupported $ref #{ref.inspect}") if match.nil?
|
|
60
|
+
|
|
61
|
+
definition_key = match[1]
|
|
62
|
+
definitions.fetch(definition_key) do
|
|
63
|
+
raise ValidationError.new(path, "unknown $ref #{ref}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_type(type_spec, value, path)
|
|
68
|
+
types = Array(type_spec).map(&:to_s)
|
|
69
|
+
return if types.any? { |type| type_match?(type, value) }
|
|
70
|
+
|
|
71
|
+
raise ValidationError.new(path, "expected #{types.join(' or ')}, got #{json_type(value)}")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def type_match?(type, value)
|
|
75
|
+
case type
|
|
76
|
+
when "null"
|
|
77
|
+
value.nil?
|
|
78
|
+
when "boolean"
|
|
79
|
+
value == true || value == false
|
|
80
|
+
when "string"
|
|
81
|
+
value.is_a?(String)
|
|
82
|
+
when "integer"
|
|
83
|
+
value.is_a?(Integer)
|
|
84
|
+
when "number"
|
|
85
|
+
value.is_a?(Numeric)
|
|
86
|
+
when "object"
|
|
87
|
+
value.is_a?(Hash)
|
|
88
|
+
when "array"
|
|
89
|
+
value.is_a?(Array)
|
|
90
|
+
else
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def json_type(value)
|
|
96
|
+
case value
|
|
97
|
+
when nil
|
|
98
|
+
"null"
|
|
99
|
+
when Hash
|
|
100
|
+
"object"
|
|
101
|
+
when Array
|
|
102
|
+
"array"
|
|
103
|
+
when String
|
|
104
|
+
"string"
|
|
105
|
+
when Integer
|
|
106
|
+
"integer"
|
|
107
|
+
when Numeric
|
|
108
|
+
"number"
|
|
109
|
+
when TrueClass, FalseClass
|
|
110
|
+
"boolean"
|
|
111
|
+
else
|
|
112
|
+
value.class.name
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_enum(options, value, path)
|
|
117
|
+
return if options.include?(value)
|
|
118
|
+
|
|
119
|
+
raise ValidationError.new(path, "must be one of #{options.inspect}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_const(expected, value, path)
|
|
123
|
+
return if value == expected
|
|
124
|
+
|
|
125
|
+
raise ValidationError.new(path, "must be #{expected.inspect}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_format(format, value, path)
|
|
129
|
+
case format
|
|
130
|
+
when "int32"
|
|
131
|
+
validate_integer_range(value, path, -(2**31), (2**31) - 1)
|
|
132
|
+
when "uint32"
|
|
133
|
+
validate_integer_range(value, path, 0, (2**32) - 1)
|
|
134
|
+
when "int64"
|
|
135
|
+
validate_integer_range(value, path, -(2**63), (2**63) - 1)
|
|
136
|
+
when "uint64"
|
|
137
|
+
validate_integer_range(value, path, 0, (2**64) - 1)
|
|
138
|
+
when "uint16"
|
|
139
|
+
validate_integer_range(value, path, 0, 65_535)
|
|
140
|
+
when "double"
|
|
141
|
+
unless value.is_a?(Numeric)
|
|
142
|
+
raise ValidationError.new(path, "must be a number for format double")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def validate_integer_range(value, path, min, max)
|
|
148
|
+
return unless value.is_a?(Integer)
|
|
149
|
+
return if value >= min && value <= max
|
|
150
|
+
|
|
151
|
+
raise ValidationError.new(path, "must be between #{min} and #{max}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def validate_numeric_bounds(schema, value, path)
|
|
155
|
+
return unless value.is_a?(Numeric)
|
|
156
|
+
|
|
157
|
+
if schema.key?("minimum") && value < schema["minimum"]
|
|
158
|
+
raise ValidationError.new(path, "must be >= #{schema['minimum']}")
|
|
159
|
+
end
|
|
160
|
+
if schema.key?("maximum") && value > schema["maximum"]
|
|
161
|
+
raise ValidationError.new(path, "must be <= #{schema['maximum']}")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def validate_string_bounds(schema, value, path)
|
|
166
|
+
return unless value.is_a?(String)
|
|
167
|
+
|
|
168
|
+
if schema.key?("minLength") && value.length < schema["minLength"]
|
|
169
|
+
raise ValidationError.new(path, "length must be >= #{schema['minLength']}")
|
|
170
|
+
end
|
|
171
|
+
if schema.key?("maxLength") && value.length > schema["maxLength"]
|
|
172
|
+
raise ValidationError.new(path, "length must be <= #{schema['maxLength']}")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_array_bounds(schema, value, path)
|
|
177
|
+
return unless value.is_a?(Array)
|
|
178
|
+
|
|
179
|
+
if schema.key?("minItems") && value.length < schema["minItems"]
|
|
180
|
+
raise ValidationError.new(path, "item count must be >= #{schema['minItems']}")
|
|
181
|
+
end
|
|
182
|
+
if schema.key?("maxItems") && value.length > schema["maxItems"]
|
|
183
|
+
raise ValidationError.new(path, "item count must be <= #{schema['maxItems']}")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate_object(schema, value, definitions, path)
|
|
188
|
+
return unless value.is_a?(Hash)
|
|
189
|
+
|
|
190
|
+
required = Array(schema["required"])
|
|
191
|
+
properties = schema["properties"].is_a?(Hash) ? schema["properties"] : {}
|
|
192
|
+
additional = schema.key?("additionalProperties") ? schema["additionalProperties"] : true
|
|
193
|
+
object = value.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v }
|
|
194
|
+
|
|
195
|
+
required.each do |key|
|
|
196
|
+
next if object.key?(key)
|
|
197
|
+
|
|
198
|
+
raise ValidationError.new(path_for_key(path, key), "is required")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
object.each do |key, nested_value|
|
|
202
|
+
nested_path = path_for_key(path, key)
|
|
203
|
+
if properties.key?(key)
|
|
204
|
+
validate_schema(properties[key], nested_value, definitions, nested_path)
|
|
205
|
+
next
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if additional == false
|
|
209
|
+
raise ValidationError.new(nested_path, "additional property is not allowed")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
validate_schema(additional, nested_value, definitions, nested_path) if additional.is_a?(Hash)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def validate_items(items_schema, value, definitions, path)
|
|
217
|
+
return unless value.is_a?(Array)
|
|
218
|
+
|
|
219
|
+
case items_schema
|
|
220
|
+
when Array
|
|
221
|
+
items_schema.each_with_index do |item_schema, index|
|
|
222
|
+
break if index >= value.length
|
|
223
|
+
|
|
224
|
+
validate_schema(item_schema, value[index], definitions, "#{path}[#{index}]")
|
|
225
|
+
end
|
|
226
|
+
else
|
|
227
|
+
value.each_with_index do |item, index|
|
|
228
|
+
validate_schema(items_schema, item, definitions, "#{path}[#{index}]")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def validate_all_of(schemas, value, definitions, path)
|
|
234
|
+
schemas.each do |sub_schema|
|
|
235
|
+
validate_schema(sub_schema, value, definitions, path)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def validate_any_of(schemas, value, definitions, path)
|
|
240
|
+
errors = []
|
|
241
|
+
schemas.each do |sub_schema|
|
|
242
|
+
validate_schema(sub_schema, value, definitions, path)
|
|
243
|
+
return
|
|
244
|
+
rescue ValidationError => e
|
|
245
|
+
errors << e
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
closest = closest_error(errors, path)
|
|
249
|
+
raise ValidationError.new(
|
|
250
|
+
path,
|
|
251
|
+
"must match anyOf (closest mismatch at #{closest.path}: #{closest.message})"
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def validate_one_of(schemas, value, definitions, path)
|
|
256
|
+
matches = 0
|
|
257
|
+
errors = []
|
|
258
|
+
|
|
259
|
+
schemas.each do |sub_schema|
|
|
260
|
+
validate_schema(sub_schema, value, definitions, path)
|
|
261
|
+
matches += 1
|
|
262
|
+
rescue ValidationError => e
|
|
263
|
+
errors << e
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
return if matches == 1
|
|
267
|
+
|
|
268
|
+
if matches.zero?
|
|
269
|
+
closest = closest_error(errors, path)
|
|
270
|
+
raise ValidationError.new(
|
|
271
|
+
path,
|
|
272
|
+
"must match exactly one schema in oneOf (closest mismatch at #{closest.path}: #{closest.message})"
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
raise ValidationError.new(path, "must match exactly one schema in oneOf (matched #{matches})")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def closest_error(errors, path)
|
|
280
|
+
errors.max_by { |error| error.path.length } || ValidationError.new(path, "is invalid")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def path_for_key(path, key)
|
|
284
|
+
key = key.to_s
|
|
285
|
+
return "#{path}.#{key}" if key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
286
|
+
|
|
287
|
+
"#{path}[#{key.inspect}]"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def protocol_version_legacy_string?(schema, value, definitions)
|
|
291
|
+
return false unless value.is_a?(String)
|
|
292
|
+
|
|
293
|
+
protocol_version_schema = definitions["ProtocolVersion"]
|
|
294
|
+
return false if protocol_version_schema.nil?
|
|
295
|
+
|
|
296
|
+
schema.equal?(protocol_version_schema) || schema == protocol_version_schema
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "agent_client_protocol/version"
|
|
4
|
+
require_relative "agent_client_protocol/protocol_version"
|
|
5
|
+
require_relative "agent_client_protocol/error"
|
|
6
|
+
require_relative "agent_client_protocol/schema_registry"
|
|
7
|
+
require_relative "agent_client_protocol/methods"
|
|
8
|
+
require_relative "agent_client_protocol/types"
|
|
9
|
+
require_relative "agent_client_protocol/type_registry"
|
|
10
|
+
require_relative "agent_client_protocol/validator"
|
|
11
|
+
require_relative "agent_client_protocol/rpc"
|
|
12
|
+
require_relative "agent_client_protocol/decoder"
|
|
13
|
+
require_relative "agent_client_protocol/codec"
|
|
14
|
+
require_relative "agent_client_protocol/constants"
|
|
15
|
+
|
|
16
|
+
module AgentClientProtocol
|
|
17
|
+
AGENT_METHOD_NAMES = Methods.agent.freeze
|
|
18
|
+
CLIENT_METHOD_NAMES = Methods.client.freeze
|
|
19
|
+
|
|
20
|
+
UNSTABLE_AGENT_METHOD_NAMES = Methods.agent(unstable: true).freeze
|
|
21
|
+
UNSTABLE_CLIENT_METHOD_NAMES = Methods.client(unstable: true).freeze
|
|
22
|
+
PROTOCOL_METHOD_NAMES = Methods.protocol(unstable: true).freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def type_for(definition_name, unstable: false)
|
|
27
|
+
TypeRegistry.fetch(definition_name, unstable: unstable)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_typed(definition_name, payload, unstable: false)
|
|
31
|
+
TypeRegistry.build(definition_name, payload, unstable: unstable)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate(definition_name, payload, unstable: false)
|
|
35
|
+
Validator.validate(definition_name, payload, unstable: unstable)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def codec(side:, unstable: false, validate_schema: true)
|
|
39
|
+
Codec.new(side: side, unstable: unstable, validate_schema: validate_schema)
|
|
40
|
+
end
|
|
41
|
+
end
|
data/schema/meta.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agentMethods": {
|
|
3
|
+
"authenticate": "authenticate",
|
|
4
|
+
"initialize": "initialize",
|
|
5
|
+
"session_cancel": "session/cancel",
|
|
6
|
+
"session_load": "session/load",
|
|
7
|
+
"session_new": "session/new",
|
|
8
|
+
"session_prompt": "session/prompt",
|
|
9
|
+
"session_set_config_option": "session/set_config_option",
|
|
10
|
+
"session_set_mode": "session/set_mode"
|
|
11
|
+
},
|
|
12
|
+
"clientMethods": {
|
|
13
|
+
"fs_read_text_file": "fs/read_text_file",
|
|
14
|
+
"fs_write_text_file": "fs/write_text_file",
|
|
15
|
+
"session_request_permission": "session/request_permission",
|
|
16
|
+
"session_update": "session/update",
|
|
17
|
+
"terminal_create": "terminal/create",
|
|
18
|
+
"terminal_kill": "terminal/kill",
|
|
19
|
+
"terminal_output": "terminal/output",
|
|
20
|
+
"terminal_release": "terminal/release",
|
|
21
|
+
"terminal_wait_for_exit": "terminal/wait_for_exit"
|
|
22
|
+
},
|
|
23
|
+
"version": 1
|
|
24
|
+
}
|