fix: enforce atomic agent registration contract

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 15:38:40 -04:00
1 parent 47d323c17b
commit f64d00673c
58 files changed
+2692 -1667

No files matched your search

+1
View File
@@ -179,6 +179,7 @@ result = client.verify_operation(operation_id)
# Register a new agent
agent = client.register_agent({
"agent_id": "my-agent",
"integration_type": "codex",
"display_name": "My Agent",
"responsible_entity": "team@example.com",
"keys": [{"kid": "key-v1", "public_key": "<base64url>", "algorithm": "ed25519"}],
+4
View File
@@ -44,6 +44,8 @@ from .types import (
GetMeResponse,
IssueTokenResponse,
HealthResponse,
INTEGRATION_TYPES,
IntegrationType,
)
__version__ = "1.0.0"
@@ -93,4 +95,6 @@ __all__ = [
"GetMeResponse",
"IssueTokenResponse",
"HealthResponse",
"INTEGRATION_TYPES",
"IntegrationType",
]
+2
View File
@@ -11,6 +11,7 @@ import aiohttp
from .crypto import compute_chain_hash, compute_payload_hash, sign_eor
from .errors import ElydoraError
from .integration_types import require_integration_type
from .types import (
AuditQueryResponse,
AuthLoginResponse,
@@ -231,6 +232,7 @@ class AsyncElydoraClient:
async def register_agent(self, request: RegisterAgentRequest) -> RegisterAgentResponse:
"""Register a new agent with the organization."""
require_integration_type(request.get("integration_type"))
return await self._request("POST", "/v1/agents/register", json_body=request)
async def get_agent(self, agent_id: str) -> GetAgentResponse:
+2
View File
@@ -10,6 +10,7 @@ import requests
from .crypto import compute_chain_hash, compute_payload_hash, sign_eor
from .errors import ElydoraError
from .integration_types import require_integration_type
from .types import (
AuditQueryResponse,
AuthLoginResponse,
@@ -222,6 +223,7 @@ class ElydoraClient:
def register_agent(self, request: RegisterAgentRequest) -> RegisterAgentResponse:
"""Register a new agent with the organization."""
require_integration_type(request.get("integration_type"))
return self._request("POST", "/v1/agents/register", json_body=request)
def get_agent(self, agent_id: str) -> GetAgentResponse:
+12
View File
@@ -0,0 +1,12 @@
"""Runtime validation for agent integration types."""
from typing import cast
from .types import INTEGRATION_TYPES, IntegrationType
def require_integration_type(value: object) -> IntegrationType:
if not isinstance(value, str) or value not in INTEGRATION_TYPES:
expected = ", ".join(INTEGRATION_TYPES)
raise ValueError(f"Invalid integration_type {value!r}. Expected one of: {expected}")
return cast(IntegrationType, value)
+34 -8
View File
@@ -3,15 +3,13 @@
All types use TypedDict for structural typing, compatible with Python 3.9+.
"""
from __future__ import annotations
import sys
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
if sys.version_info >= (3, 11):
from typing import Literal, TypedDict
from typing import Literal, NotRequired, TypedDict
else:
from typing_extensions import Literal, TypedDict
from typing_extensions import Literal, NotRequired, TypedDict
# ---------------------------------------------------------------------------
@@ -28,6 +26,32 @@ RbacRole = Literal[
"readonly_investigator",
"integration_engineer",
]
IntegrationType = Literal[
"augment",
"claudecode",
"cline",
"codex",
"copilot",
"cursor",
"droid",
"gemini",
"grok",
"kimi",
"kirocli",
"kiroide",
"letta",
"opencode",
"qwen",
"enterprise",
"gui",
"sdk",
"other",
]
INTEGRATION_TYPES: Tuple[IntegrationType, ...] = (
"augment", "claudecode", "cline", "codex", "copilot", "cursor", "droid",
"gemini", "grok", "kimi", "kirocli", "kiroide", "letta", "opencode", "qwen",
"enterprise", "gui", "sdk", "other",
)
ErrorCode = Literal[
"INVALID_SIGNATURE",
"UNKNOWN_AGENT",
@@ -55,6 +79,7 @@ class Agent(TypedDict):
org_id: str
display_name: str
responsible_entity: str
integration_type: IntegrationType
status: AgentStatus
created_at: int
updated_at: int
@@ -187,11 +212,12 @@ class RegisterAgentKeyParam(TypedDict):
algorithm: Literal["ed25519"]
class RegisterAgentRequest(TypedDict, total=False):
class RegisterAgentRequest(TypedDict):
agent_id: str
display_name: str
responsible_entity: str
integration_type: IntegrationType
keys: List[RegisterAgentKeyParam]
display_name: NotRequired[str]
responsible_entity: NotRequired[str]
class AuditQueryParams(TypedDict, total=False):
@@ -0,0 +1,137 @@
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import get_args
import pytest
from elydora import (
AsyncElydoraClient,
ElydoraClient,
INTEGRATION_TYPES,
IntegrationType,
RegisterAgentRequest,
)
EXPECTED_INTEGRATION_TYPES = (
"augment", "claudecode", "cline", "codex", "copilot", "cursor", "droid",
"gemini", "grok", "kimi", "kirocli", "kiroide", "letta", "opencode", "qwen",
"enterprise", "gui", "sdk", "other",
)
class RegistrationHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
content_length = int(self.headers["Content-Length"])
body = json.loads(self.rfile.read(content_length))
self.server.requests.append({
"method": self.command,
"path": self.path,
"authorization": self.headers.get("Authorization"),
"body": body,
})
response = json.dumps({
"agent": {
"agent_id": body["agent_id"],
"org_id": "org-1",
"display_name": body.get("display_name", ""),
"responsible_entity": body.get("responsible_entity", ""),
"integration_type": body["integration_type"],
"status": "active",
"created_at": 1,
"updated_at": 1,
},
"keys": [],
}).encode()
self.send_response(201)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(response)))
self.end_headers()
self.wfile.write(response)
def log_message(self, _format: str, *args: object) -> None:
pass
@pytest.fixture
def registration_server():
server = ThreadingHTTPServer(("127.0.0.1", 0), RegistrationHandler)
server.requests = []
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield server
finally:
server.shutdown()
server.server_close()
thread.join()
def registration_request():
return {
"agent_id": "agent-1",
"integration_type": "grok",
"display_name": "Grok Agent",
"responsible_entity": "platform@example.com",
"keys": [{"kid": "key-v1", "public_key": "public-key", "algorithm": "ed25519"}],
}
def expected_http_request():
return {
"method": "POST",
"path": "/v1/agents/register",
"authorization": "Bearer api-token",
"body": registration_request(),
}
def test_integration_types_match_public_api_contract() -> None:
assert INTEGRATION_TYPES == EXPECTED_INTEGRATION_TYPES
assert get_args(IntegrationType) == EXPECTED_INTEGRATION_TYPES
assert RegisterAgentRequest.__required_keys__ == frozenset({
"agent_id", "integration_type", "keys",
})
assert RegisterAgentRequest.__optional_keys__ == frozenset({
"display_name", "responsible_entity",
})
def test_sync_registration_requires_and_sends_integration_type(registration_server) -> None:
host, port = registration_server.server_address
client = ElydoraClient(
"org-1", "admin-agent", "unused",
base_url=f"http://{host}:{port}", max_retries=1, token="api-token",
)
with pytest.raises(ValueError, match="Invalid integration_type None"):
client.register_agent({"agent_id": "agent-1", "keys": []})
with pytest.raises(ValueError, match="Invalid integration_type 'future-cli'"):
client.register_agent({
"agent_id": "agent-1", "integration_type": "future-cli", "keys": [],
})
assert registration_server.requests == []
response = client.register_agent(registration_request())
assert response["agent"]["integration_type"] == "grok"
assert registration_server.requests == [expected_http_request()]
@pytest.mark.asyncio
async def test_async_registration_requires_and_sends_integration_type(registration_server) -> None:
host, port = registration_server.server_address
client = AsyncElydoraClient(
"org-1", "admin-agent", "unused",
base_url=f"http://{host}:{port}", max_retries=1, token="api-token",
)
try:
with pytest.raises(ValueError, match="Invalid integration_type None"):
await client.register_agent({"agent_id": "agent-1", "keys": []})
assert registration_server.requests == []
response = await client.register_agent(registration_request())
assert response["agent"]["integration_type"] == "grok"
assert registration_server.requests == [expected_http_request()]
finally:
await client.close()