Skip to content

Commit f73bda9

Browse files
authored
Merge pull request #50 from yangbaechu/feat/registry-probe-headers
feat(conformance): support optional request headers for registry probes
2 parents f606687 + 47042e5 commit f73bda9

2 files changed

Lines changed: 67 additions & 10 deletions

File tree

conformance/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ Probes and validates a live running Agent Registry REST API server.
5353
./bin/conformance-test registry http://localhost:9010/api
5454
```
5555

56+
For private or self-hosted registries that require request headers, pass one or more `--header` options. Headers are applied to all Registry API probes (`GET /agents`, `POST /search`, and `POST /explore`):
57+
58+
```bash
59+
ARD_REGISTRY_TOKEN=...
60+
./bin/conformance-test registry https://registry.example.com/api/ard \
61+
--header "Authorization: Bearer ${ARD_REGISTRY_TOKEN}"
62+
```
63+
64+
This is a conformance tooling option only; it does not require authentication for public registries or define a Registry API security model.
65+
5666
---
5767

5868
## 🔍 What It Validates

conformance/bin/conformance-test

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ def print_warning(msg):
3737
def print_bullet(msg):
3838
print(f" • {msg}")
3939

40+
def parse_request_headers(header_args):
41+
headers = {}
42+
for raw_header in header_args:
43+
if ":" not in raw_header:
44+
raise ValueError(f"Invalid header '{raw_header}'. Expected 'Name: value' format.")
45+
46+
name, value = raw_header.split(":", 1)
47+
name = name.strip()
48+
value = value.strip()
49+
50+
if not name:
51+
raise ValueError(f"Invalid header '{raw_header}'. Header name cannot be empty.")
52+
if "\r" in name or "\n" in name or "\r" in value or "\n" in value:
53+
raise ValueError(f"Invalid header '{raw_header}'. Header values cannot contain line breaks.")
54+
55+
headers[name] = value
56+
57+
return headers
58+
4059
class ConformanceTester:
4160
def __init__(self):
4261
self.errors = []
@@ -190,15 +209,16 @@ class ConformanceTester:
190209

191210
return len(self.errors) == 0
192211

193-
def validate_registry(self, registry_base_url):
212+
def validate_registry(self, registry_base_url, request_headers=None):
194213
print_header(f"Validating Agent Registry: {registry_base_url}")
195214
base_url = registry_base_url.rstrip('/')
215+
request_headers = request_headers or {}
196216

197217
# 1. Test GET /agents (Optional deterministic listing)
198218
agents_url = f"{base_url}/agents"
199219
print(f"\n {COLOR_BOLD}Probing GET /agents...{COLOR_RESET}")
200220
try:
201-
req = urllib.request.Request(agents_url, method="GET")
221+
req = urllib.request.Request(agents_url, headers=request_headers, method="GET")
202222
with urllib.request.urlopen(req, timeout=5) as response:
203223
body = response.read().decode('utf-8')
204224
data = json.loads(body)
@@ -235,10 +255,12 @@ class ConformanceTester:
235255

236256
try:
237257
req_data = json.dumps(mock_search_query).encode('utf-8')
258+
headers = dict(request_headers)
259+
headers["Content-Type"] = "application/json"
238260
req = urllib.request.Request(
239-
search_url,
240-
data=req_data,
241-
headers={"Content-Type": "application/json"},
261+
search_url,
262+
data=req_data,
263+
headers=headers,
242264
method="POST"
243265
)
244266

@@ -308,10 +330,12 @@ class ConformanceTester:
308330

309331
try:
310332
req_data = json.dumps(mock_explore_query).encode('utf-8')
333+
headers = dict(request_headers)
334+
headers["Content-Type"] = "application/json"
311335
req = urllib.request.Request(
312-
explore_url,
313-
data=req_data,
314-
headers={"Content-Type": "application/json"},
336+
explore_url,
337+
data=req_data,
338+
headers=headers,
315339
method="POST"
316340
)
317341

@@ -351,14 +375,18 @@ def main():
351375
print(f"{COLOR_BOLD}Agentic Resource Discovery Conformance CLI v0.5.0{COLOR_RESET}")
352376
print("Usage:")
353377
print(" conformance-test manifest <local_file_path_or_url>")
354-
print(" conformance-test registry <registry_base_url>")
378+
print(" conformance-test registry <registry_base_url> [--header 'Name: value']...")
355379
sys.exit(1)
356380

357381
command = sys.argv[1].lower()
358382
target = sys.argv[2]
383+
args = sys.argv[3:]
359384
tester = ConformanceTester()
360385

361386
if command == "manifest":
387+
if args:
388+
print_failure(f"Unexpected arguments for manifest command: {' '.join(args)}")
389+
sys.exit(1)
362390
# Handle remote url vs local path
363391
if target.startswith("http://") or target.startswith("https://"):
364392
try:
@@ -381,7 +409,26 @@ def main():
381409
print_failure(f"Failed to read local file: {e}")
382410
sys.exit(1)
383411
elif command == "registry":
384-
success = tester.validate_registry(target)
412+
header_args = []
413+
idx = 0
414+
while idx < len(args):
415+
arg = args[idx]
416+
if arg != "--header":
417+
print_failure(f"Unknown registry option '{arg}'. Supported option: --header 'Name: value'")
418+
sys.exit(1)
419+
if idx + 1 >= len(args):
420+
print_failure("--header requires a value in 'Name: value' format.")
421+
sys.exit(1)
422+
header_args.append(args[idx + 1])
423+
idx += 2
424+
425+
try:
426+
request_headers = parse_request_headers(header_args)
427+
except ValueError as e:
428+
print_failure(str(e))
429+
sys.exit(1)
430+
431+
success = tester.validate_registry(target, request_headers=request_headers)
385432
else:
386433
print_failure(f"Unknown test suite command '{command}'. Must be 'manifest' or 'registry'.")
387434
sys.exit(1)

0 commit comments

Comments
 (0)