agentsid 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/agentsid.gemspec +26 -0
- data/lib/agentsid/client.rb +197 -0
- data/lib/agentsid/errors.rb +45 -0
- data/lib/agentsid/middleware.rb +130 -0
- data/lib/agentsid.rb +11 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 796f78999826a0414ab444d8064b49b86225a2b756709b3f2fe87b3ba237232b
|
|
4
|
+
data.tar.gz: 358114838c3cbb6e50eb24bb51afcb6af141ee1c9bb646821dde80f3686d3cc8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c36cc8d5ffff7db25ce08b52d9f0097cfb4332ee1c87f05591c60887e7e93fa86202e0ddc69f7d79bb873bef0562433a2022e3e5a64cd3652362ba99d4477059
|
|
7
|
+
data.tar.gz: 6cf73da1f2342d7a58ec313ed07fd7753b292e1e3c17306d182c7f717a4b02b48d4610078e230b15deb3f0885efb97ee640d556d8020b211f245cad88c5fd3b4
|
data/agentsid.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "agentsid"
|
|
5
|
+
spec.version = "0.1.0"
|
|
6
|
+
spec.authors = ["AgentsID"]
|
|
7
|
+
spec.email = ["support@agentsid.dev"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Ruby SDK for AgentsID — agent identity, permissions, and audit for AI tools"
|
|
10
|
+
spec.description = "Register AI agents, issue scoped tokens, enforce per-tool permissions, " \
|
|
11
|
+
"and query audit logs via the AgentsID API. Includes MCP middleware for " \
|
|
12
|
+
"validating tool calls in MCP servers."
|
|
13
|
+
spec.homepage = "https://agentsid.dev"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
17
|
+
|
|
18
|
+
spec.files = Dir["lib/**/*.rb"] + ["agentsid.gemspec"]
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.metadata = {
|
|
22
|
+
"homepage_uri" => spec.homepage,
|
|
23
|
+
"source_code_uri" => "https://github.com/agentsid/sdk-ruby",
|
|
24
|
+
"bug_tracker_uri" => "https://github.com/agentsid/sdk-ruby/issues"
|
|
25
|
+
}
|
|
26
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
|
|
9
|
+
module AgentsID
|
|
10
|
+
DEFAULT_BASE_URL = "https://agentsid.dev"
|
|
11
|
+
DEFAULT_TIMEOUT = 10
|
|
12
|
+
|
|
13
|
+
# AgentsID client -- register agents, validate tokens, manage permissions.
|
|
14
|
+
class Client
|
|
15
|
+
# @param project_key [String] Your AgentsID project key (aid_proj_...)
|
|
16
|
+
# @param base_url [String] AgentsID server URL
|
|
17
|
+
# @param timeout [Integer] HTTP timeout in seconds
|
|
18
|
+
def initialize(project_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
19
|
+
raise AgentsIDError.new("project_key is required", code: "CONFIG_ERROR") if project_key.nil? || project_key.empty?
|
|
20
|
+
|
|
21
|
+
@project_key = project_key
|
|
22
|
+
@base_url = base_url.chomp("/")
|
|
23
|
+
@timeout = timeout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# -----------------------------------------------------------
|
|
27
|
+
# AGENTS
|
|
28
|
+
# -----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
# Register a new agent identity and issue a token.
|
|
31
|
+
#
|
|
32
|
+
# @param name [String]
|
|
33
|
+
# @param on_behalf_of [String]
|
|
34
|
+
# @param permissions [Array<String>, nil]
|
|
35
|
+
# @param ttl_hours [Integer, nil]
|
|
36
|
+
# @param metadata [Hash, nil]
|
|
37
|
+
# @return [Hash]
|
|
38
|
+
def register_agent(name:, on_behalf_of:, permissions: nil, ttl_hours: nil, metadata: nil)
|
|
39
|
+
request(:post, "/agents/", {
|
|
40
|
+
name: name,
|
|
41
|
+
on_behalf_of: on_behalf_of,
|
|
42
|
+
permissions: permissions,
|
|
43
|
+
ttl_hours: ttl_hours,
|
|
44
|
+
metadata: metadata
|
|
45
|
+
})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param agent_id [String]
|
|
49
|
+
# @return [Hash]
|
|
50
|
+
def get_agent(agent_id)
|
|
51
|
+
request(:get, "/agents/#{agent_id}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param status [String, nil] Filter by status
|
|
55
|
+
# @param limit [Integer] Max results (default 50)
|
|
56
|
+
# @return [Array<Hash>]
|
|
57
|
+
def list_agents(status: nil, limit: 50)
|
|
58
|
+
params = {}
|
|
59
|
+
params[:status] = status if status
|
|
60
|
+
params[:limit] = limit if limit != 50
|
|
61
|
+
qs = URI.encode_www_form(params)
|
|
62
|
+
path = qs.empty? ? "/agents/" : "/agents/?#{qs}"
|
|
63
|
+
request(:get, path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param agent_id [String]
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
def revoke_agent(agent_id)
|
|
69
|
+
request(:delete, "/agents/#{agent_id}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# -----------------------------------------------------------
|
|
73
|
+
# PERMISSIONS
|
|
74
|
+
# -----------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
# Set permission rules for an agent.
|
|
77
|
+
#
|
|
78
|
+
# @param agent_id [String]
|
|
79
|
+
# @param rules [Array<Hash>] Each rule: { tool_pattern: "...", action: "allow"|"deny" }
|
|
80
|
+
# @return [Hash]
|
|
81
|
+
def set_permissions(agent_id, rules)
|
|
82
|
+
body = rules.map do |r|
|
|
83
|
+
{
|
|
84
|
+
tool_pattern: r[:tool_pattern] || r["tool_pattern"] || r[:toolPattern] || r["toolPattern"] || "",
|
|
85
|
+
action: r[:action] || r["action"] || "allow",
|
|
86
|
+
conditions: r[:conditions] || r["conditions"],
|
|
87
|
+
priority: r[:priority] || r["priority"] || 0
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
request(:put, "/agents/#{agent_id}/permissions", body)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param agent_id [String]
|
|
94
|
+
# @return [Array<Hash>]
|
|
95
|
+
def get_permissions(agent_id)
|
|
96
|
+
data = request(:get, "/agents/#{agent_id}/permissions")
|
|
97
|
+
data.fetch("rules", [])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @param agent_id [String]
|
|
101
|
+
# @param tool [String]
|
|
102
|
+
# @param params [Hash, nil]
|
|
103
|
+
# @return [Hash]
|
|
104
|
+
def check_permission(agent_id, tool, params: nil)
|
|
105
|
+
request(:post, "/check", {
|
|
106
|
+
agent_id: agent_id,
|
|
107
|
+
tool: tool,
|
|
108
|
+
params: params
|
|
109
|
+
})
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# -----------------------------------------------------------
|
|
113
|
+
# TOKEN VALIDATION
|
|
114
|
+
# -----------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
# @param token [String]
|
|
117
|
+
# @param tool [String, nil]
|
|
118
|
+
# @param params [Hash, nil]
|
|
119
|
+
# @return [Hash]
|
|
120
|
+
def validate_token(token, tool: nil, params: nil)
|
|
121
|
+
request(:post, "/validate", {
|
|
122
|
+
token: token,
|
|
123
|
+
tool: tool,
|
|
124
|
+
params: params
|
|
125
|
+
})
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# -----------------------------------------------------------
|
|
129
|
+
# AUDIT
|
|
130
|
+
# -----------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
# @param agent_id [String, nil]
|
|
133
|
+
# @param tool [String, nil]
|
|
134
|
+
# @param action [String, nil]
|
|
135
|
+
# @param since [String, nil] ISO 8601 timestamp
|
|
136
|
+
# @param limit [Integer]
|
|
137
|
+
# @return [Hash]
|
|
138
|
+
def get_audit_log(agent_id: nil, tool: nil, action: nil, since: nil, limit: 100)
|
|
139
|
+
params = {}
|
|
140
|
+
params[:agent_id] = agent_id if agent_id
|
|
141
|
+
params[:tool] = tool if tool
|
|
142
|
+
params[:action] = action if action
|
|
143
|
+
params[:since] = since if since
|
|
144
|
+
params[:limit] = limit
|
|
145
|
+
qs = URI.encode_www_form(params)
|
|
146
|
+
request(:get, "/audit/?#{qs}")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def request(method, path, body = nil)
|
|
152
|
+
uri = URI("#{@base_url}/api/v1#{path}")
|
|
153
|
+
|
|
154
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
155
|
+
http.use_ssl = uri.scheme == "https"
|
|
156
|
+
http.open_timeout = @timeout
|
|
157
|
+
http.read_timeout = @timeout
|
|
158
|
+
|
|
159
|
+
req = build_request(method, uri, body)
|
|
160
|
+
|
|
161
|
+
response = http.request(req)
|
|
162
|
+
|
|
163
|
+
handle_response(response)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build_request(method, uri, body)
|
|
167
|
+
klass = {
|
|
168
|
+
get: Net::HTTP::Get,
|
|
169
|
+
post: Net::HTTP::Post,
|
|
170
|
+
put: Net::HTTP::Put,
|
|
171
|
+
delete: Net::HTTP::Delete
|
|
172
|
+
}.fetch(method)
|
|
173
|
+
|
|
174
|
+
req = klass.new(uri)
|
|
175
|
+
req["Authorization"] = "Bearer #{@project_key}"
|
|
176
|
+
req["Content-Type"] = "application/json"
|
|
177
|
+
req.body = JSON.generate(body) if body
|
|
178
|
+
req
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def handle_response(response)
|
|
182
|
+
status = response.code.to_i
|
|
183
|
+
|
|
184
|
+
raise AuthenticationError.new if status == 401
|
|
185
|
+
return {} if status == 204
|
|
186
|
+
|
|
187
|
+
data = JSON.parse(response.body)
|
|
188
|
+
|
|
189
|
+
unless (200..299).cover?(status)
|
|
190
|
+
message = data["detail"] || "Request failed: #{status}"
|
|
191
|
+
raise AgentsIDError.new(message, code: "API_ERROR", status_code: status)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
data
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentsID
|
|
4
|
+
class AgentsIDError < StandardError
|
|
5
|
+
attr_reader :code, :status_code
|
|
6
|
+
|
|
7
|
+
def initialize(message, code: "UNKNOWN", status_code: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@code = code
|
|
10
|
+
@status_code = status_code
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AuthenticationError < AgentsIDError
|
|
15
|
+
def initialize(message = "Invalid or missing API key")
|
|
16
|
+
super(message, code: "AUTH_ERROR", status_code: 401)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class PermissionDeniedError < AgentsIDError
|
|
21
|
+
attr_reader :tool, :reason
|
|
22
|
+
|
|
23
|
+
def initialize(tool, reason)
|
|
24
|
+
@tool = tool
|
|
25
|
+
@reason = reason
|
|
26
|
+
super(
|
|
27
|
+
"Permission denied for tool \"#{tool}\": #{reason}",
|
|
28
|
+
code: "PERMISSION_DENIED",
|
|
29
|
+
status_code: 403
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class TokenExpiredError < AgentsIDError
|
|
35
|
+
def initialize
|
|
36
|
+
super("Agent token has expired", code: "TOKEN_EXPIRED", status_code: 401)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class TokenRevokedError < AgentsIDError
|
|
41
|
+
def initialize
|
|
42
|
+
super("Agent token has been revoked", code: "TOKEN_REVOKED", status_code: 401)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
|
|
9
|
+
module AgentsID
|
|
10
|
+
DEFAULT_BASE_URL_MW = "https://agentsid.dev"
|
|
11
|
+
|
|
12
|
+
# Validate a single tool call against AgentsID.
|
|
13
|
+
#
|
|
14
|
+
# @param project_key [String]
|
|
15
|
+
# @param token [String]
|
|
16
|
+
# @param tool [String]
|
|
17
|
+
# @param params [Hash, nil]
|
|
18
|
+
# @param base_url [String]
|
|
19
|
+
# @return [Hash]
|
|
20
|
+
def self.validate_tool_call(project_key:, token:, tool:, params: nil, base_url: DEFAULT_BASE_URL_MW)
|
|
21
|
+
uri = URI("#{base_url.chomp('/')}/api/v1/validate")
|
|
22
|
+
|
|
23
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
24
|
+
http.use_ssl = uri.scheme == "https"
|
|
25
|
+
http.open_timeout = 10
|
|
26
|
+
http.read_timeout = 10
|
|
27
|
+
|
|
28
|
+
req = Net::HTTP::Post.new(uri)
|
|
29
|
+
req["Content-Type"] = "application/json"
|
|
30
|
+
req.body = JSON.generate({ token: token, tool: tool, params: params })
|
|
31
|
+
|
|
32
|
+
response = http.request(req)
|
|
33
|
+
status = response.code.to_i
|
|
34
|
+
|
|
35
|
+
unless (200..299).cover?(status)
|
|
36
|
+
return { "valid" => false, "reason" => "Validation failed: #{status}" }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
JSON.parse(response.body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# MCP middleware -- validates tool calls against AgentsID.
|
|
43
|
+
#
|
|
44
|
+
# Usage:
|
|
45
|
+
# middleware = AgentsID::MCPMiddleware.new(project_key: "aid_proj_...")
|
|
46
|
+
#
|
|
47
|
+
# # In your MCP tool handler:
|
|
48
|
+
# result = middleware.validate(bearer_token, "my_tool", params)
|
|
49
|
+
class MCPMiddleware
|
|
50
|
+
# @param project_key [String] Your AgentsID project key
|
|
51
|
+
# @param base_url [String] AgentsID server URL
|
|
52
|
+
# @param skip_tools [Array<String>, nil] Tool names to skip validation for
|
|
53
|
+
# @param on_denied [Proc, nil] Callback invoked on denial instead of raising
|
|
54
|
+
def initialize(project_key:, base_url: DEFAULT_BASE_URL_MW, skip_tools: nil, on_denied: nil)
|
|
55
|
+
@project_key = project_key
|
|
56
|
+
@base_url = base_url.chomp("/")
|
|
57
|
+
@skip_tools = Set.new(skip_tools || [])
|
|
58
|
+
@on_denied = on_denied
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate a tool call. Raises on denial unless on_denied is set.
|
|
62
|
+
#
|
|
63
|
+
# @param token [String]
|
|
64
|
+
# @param tool [String]
|
|
65
|
+
# @param params [Hash, nil]
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
def validate(token, tool, params = nil)
|
|
68
|
+
if @skip_tools.include?(tool)
|
|
69
|
+
return { "valid" => true, "reason" => "Tool in skip list" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
result = AgentsID.validate_tool_call(
|
|
73
|
+
project_key: @project_key,
|
|
74
|
+
token: token,
|
|
75
|
+
tool: tool,
|
|
76
|
+
params: params,
|
|
77
|
+
base_url: @base_url
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
unless result["valid"]
|
|
81
|
+
reason = result["reason"] || "Unknown"
|
|
82
|
+
raise TokenExpiredError.new if reason.include?("expired")
|
|
83
|
+
raise TokenRevokedError.new if reason.include?("revoked")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
permission = result["permission"] || {}
|
|
87
|
+
if permission.any? && !permission["allowed"]
|
|
88
|
+
denial_reason = permission["reason"] || "Denied"
|
|
89
|
+
if @on_denied
|
|
90
|
+
@on_denied.call(tool, denial_reason)
|
|
91
|
+
else
|
|
92
|
+
raise PermissionDeniedError.new(tool, denial_reason)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Quick check -- returns true/false without raising.
|
|
100
|
+
#
|
|
101
|
+
# @param token [String]
|
|
102
|
+
# @param tool [String]
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def allowed?(token, tool)
|
|
105
|
+
result = AgentsID.validate_tool_call(
|
|
106
|
+
project_key: @project_key,
|
|
107
|
+
token: token,
|
|
108
|
+
tool: tool,
|
|
109
|
+
base_url: @base_url
|
|
110
|
+
)
|
|
111
|
+
result["valid"] == true && result.dig("permission", "allowed") == true
|
|
112
|
+
rescue StandardError
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Factory method matching the Python SDK's create_mcp_middleware.
|
|
118
|
+
#
|
|
119
|
+
# @param project_key [String]
|
|
120
|
+
# @param base_url [String]
|
|
121
|
+
# @param skip_tools [Array<String>, nil]
|
|
122
|
+
# @return [MCPMiddleware]
|
|
123
|
+
def self.create_mcp_middleware(project_key:, base_url: DEFAULT_BASE_URL_MW, skip_tools: nil)
|
|
124
|
+
MCPMiddleware.new(
|
|
125
|
+
project_key: project_key,
|
|
126
|
+
base_url: base_url,
|
|
127
|
+
skip_tools: skip_tools
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/agentsid.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: agentsid
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- AgentsID
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Register AI agents, issue scoped tokens, enforce per-tool permissions,
|
|
14
|
+
and query audit logs via the AgentsID API. Includes MCP middleware for validating
|
|
15
|
+
tool calls in MCP servers.
|
|
16
|
+
email:
|
|
17
|
+
- support@agentsid.dev
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- agentsid.gemspec
|
|
23
|
+
- lib/agentsid.rb
|
|
24
|
+
- lib/agentsid/client.rb
|
|
25
|
+
- lib/agentsid/errors.rb
|
|
26
|
+
- lib/agentsid/middleware.rb
|
|
27
|
+
homepage: https://agentsid.dev
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://agentsid.dev
|
|
32
|
+
source_code_uri: https://github.com/agentsid/sdk-ruby
|
|
33
|
+
bug_tracker_uri: https://github.com/agentsid/sdk-ruby/issues
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 3.0.0
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.0.3.1
|
|
50
|
+
signing_key:
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Ruby SDK for AgentsID — agent identity, permissions, and audit for AI tools
|
|
53
|
+
test_files: []
|