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
@@ -189,6 +189,7 @@ agent, err := client.RegisterAgent(&elydora.RegisterAgentRequest{
AgentID: "my-agent",
DisplayName: "My Agent",
ResponsibleEntity: "team@example.com",
IntegrationType: elydora.IntegrationTypeSDK,
Keys: []elydora.RegisterAgentKeyInput{
{KID: "key-v1", PublicKey: "<base64url>", Algorithm: "ed25519"},
},
+7
View File
@@ -7,6 +7,13 @@ import (
// RegisterAgent registers a new AI agent within the organization.
func (c *Client) RegisterAgent(req *RegisterAgentRequest) (*RegisterAgentResponse, error) {
if req == nil {
return nil, fmt.Errorf("elydora: register agent request must not be nil")
}
if !req.IntegrationType.IsValid() {
return nil, fmt.Errorf("elydora: invalid integration_type %q", req.IntegrationType)
}
var result RegisterAgentResponse
if err := c.doPost("/v1/agents/register", req, &result); err != nil {
return nil, err
+124 -70
View File
@@ -20,6 +20,58 @@ const (
KeyStatusRevoked KeyStatus = "revoked"
)
type IntegrationType string
const (
IntegrationTypeAugment IntegrationType = "augment"
IntegrationTypeClaudecode IntegrationType = "claudecode"
IntegrationTypeCline IntegrationType = "cline"
IntegrationTypeCodex IntegrationType = "codex"
IntegrationTypeCopilot IntegrationType = "copilot"
IntegrationTypeCursor IntegrationType = "cursor"
IntegrationTypeDroid IntegrationType = "droid"
IntegrationTypeGemini IntegrationType = "gemini"
IntegrationTypeGrok IntegrationType = "grok"
IntegrationTypeKimi IntegrationType = "kimi"
IntegrationTypeKiroCLI IntegrationType = "kirocli"
IntegrationTypeKiroIDE IntegrationType = "kiroide"
IntegrationTypeLetta IntegrationType = "letta"
IntegrationTypeOpenCode IntegrationType = "opencode"
IntegrationTypeQwen IntegrationType = "qwen"
IntegrationTypeEnterprise IntegrationType = "enterprise"
IntegrationTypeGUI IntegrationType = "gui"
IntegrationTypeSDK IntegrationType = "sdk"
IntegrationTypeOther IntegrationType = "other"
)
// IsValid reports whether the integration type belongs to the public registration contract.
func (integrationType IntegrationType) IsValid() bool {
switch integrationType {
case IntegrationTypeAugment,
IntegrationTypeClaudecode,
IntegrationTypeCline,
IntegrationTypeCodex,
IntegrationTypeCopilot,
IntegrationTypeCursor,
IntegrationTypeDroid,
IntegrationTypeGemini,
IntegrationTypeGrok,
IntegrationTypeKimi,
IntegrationTypeKiroCLI,
IntegrationTypeKiroIDE,
IntegrationTypeLetta,
IntegrationTypeOpenCode,
IntegrationTypeQwen,
IntegrationTypeEnterprise,
IntegrationTypeGUI,
IntegrationTypeSDK,
IntegrationTypeOther:
return true
default:
return false
}
}
type ExportStatus string
const (
@@ -32,30 +84,30 @@ const (
type RbacRole string
const (
RbacRoleOrgOwner RbacRole = "org_owner"
RbacRoleSecurityAdmin RbacRole = "security_admin"
RbacRoleComplianceAuditor RbacRole = "compliance_auditor"
RbacRoleReadonlyInvestigator RbacRole = "readonly_investigator"
RbacRoleIntegrationEngineer RbacRole = "integration_engineer"
RbacRoleOrgOwner RbacRole = "org_owner"
RbacRoleSecurityAdmin RbacRole = "security_admin"
RbacRoleComplianceAuditor RbacRole = "compliance_auditor"
RbacRoleReadonlyInvestigator RbacRole = "readonly_investigator"
RbacRoleIntegrationEngineer RbacRole = "integration_engineer"
)
type ErrorCode string
const (
ErrorCodeInvalidSignature ErrorCode = "INVALID_SIGNATURE"
ErrorCodeUnknownAgent ErrorCode = "UNKNOWN_AGENT"
ErrorCodeKeyRevoked ErrorCode = "KEY_REVOKED"
ErrorCodeAgentFrozen ErrorCode = "AGENT_FROZEN"
ErrorCodeTTLExpired ErrorCode = "TTL_EXPIRED"
ErrorCodeReplayDetected ErrorCode = "REPLAY_DETECTED"
ErrorCodePrevHashMismatch ErrorCode = "PREV_HASH_MISMATCH"
ErrorCodePayloadTooLarge ErrorCode = "PAYLOAD_TOO_LARGE"
ErrorCodeRateLimited ErrorCode = "RATE_LIMITED"
ErrorCodeInternalError ErrorCode = "INTERNAL_ERROR"
ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED"
ErrorCodeForbidden ErrorCode = "FORBIDDEN"
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
ErrorCodeValidationError ErrorCode = "VALIDATION_ERROR"
ErrorCodeInvalidSignature ErrorCode = "INVALID_SIGNATURE"
ErrorCodeUnknownAgent ErrorCode = "UNKNOWN_AGENT"
ErrorCodeKeyRevoked ErrorCode = "KEY_REVOKED"
ErrorCodeAgentFrozen ErrorCode = "AGENT_FROZEN"
ErrorCodeTTLExpired ErrorCode = "TTL_EXPIRED"
ErrorCodeReplayDetected ErrorCode = "REPLAY_DETECTED"
ErrorCodePrevHashMismatch ErrorCode = "PREV_HASH_MISMATCH"
ErrorCodePayloadTooLarge ErrorCode = "PAYLOAD_TOO_LARGE"
ErrorCodeRateLimited ErrorCode = "RATE_LIMITED"
ErrorCodeInternalError ErrorCode = "INTERNAL_ERROR"
ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED"
ErrorCodeForbidden ErrorCode = "FORBIDDEN"
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
ErrorCodeValidationError ErrorCode = "VALIDATION_ERROR"
)
type ExportFormat string
@@ -70,13 +122,14 @@ const (
// ---------------------------------------------------------------------------
type Agent struct {
AgentID string `json:"agent_id"`
OrgID string `json:"org_id"`
DisplayName string `json:"display_name"`
ResponsibleEntity string `json:"responsible_entity"`
Status AgentStatus `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
AgentID string `json:"agent_id"`
OrgID string `json:"org_id"`
DisplayName string `json:"display_name"`
ResponsibleEntity string `json:"responsible_entity"`
IntegrationType IntegrationType `json:"integration_type"`
Status AgentStatus `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
type AgentKey struct {
@@ -90,23 +143,23 @@ type AgentKey struct {
}
type Operation struct {
OperationID string `json:"operation_id"`
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
SeqNo int64 `json:"seq_no"`
OperationType string `json:"operation_type"`
IssuedAt int64 `json:"issued_at"`
TTLMs int64 `json:"ttl_ms"`
Nonce string `json:"nonce"`
Subject string `json:"subject"`
Action string `json:"action"`
PayloadHash string `json:"payload_hash"`
PrevChainHash string `json:"prev_chain_hash"`
ChainHash string `json:"chain_hash"`
AgentPubkeyKID string `json:"agent_pubkey_kid"`
Signature string `json:"signature"`
R2PayloadKey *string `json:"r2_payload_key"`
CreatedAt int64 `json:"created_at"`
OperationID string `json:"operation_id"`
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
SeqNo int64 `json:"seq_no"`
OperationType string `json:"operation_type"`
IssuedAt int64 `json:"issued_at"`
TTLMs int64 `json:"ttl_ms"`
Nonce string `json:"nonce"`
Subject string `json:"subject"`
Action string `json:"action"`
PayloadHash string `json:"payload_hash"`
PrevChainHash string `json:"prev_chain_hash"`
ChainHash string `json:"chain_hash"`
AgentPubkeyKID string `json:"agent_pubkey_kid"`
Signature string `json:"signature"`
R2PayloadKey *string `json:"r2_payload_key"`
CreatedAt int64 `json:"created_at"`
}
type Receipt struct {
@@ -161,36 +214,36 @@ type Export struct {
// EOR is the Elydora Operation Record — the fundamental unit of auditable activity.
type EOR struct {
OpVersion string `json:"op_version"`
OperationID string `json:"operation_id"`
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
IssuedAt int64 `json:"issued_at"`
TTLMs int64 `json:"ttl_ms"`
Nonce string `json:"nonce"`
OperationType string `json:"operation_type"`
Subject interface{} `json:"subject"`
Action interface{} `json:"action"`
Payload interface{} `json:"payload"`
PayloadHash string `json:"payload_hash"`
PrevChainHash string `json:"prev_chain_hash"`
AgentPubkeyKID string `json:"agent_pubkey_kid"`
Signature string `json:"signature"`
OpVersion string `json:"op_version"`
OperationID string `json:"operation_id"`
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
IssuedAt int64 `json:"issued_at"`
TTLMs int64 `json:"ttl_ms"`
Nonce string `json:"nonce"`
OperationType string `json:"operation_type"`
Subject interface{} `json:"subject"`
Action interface{} `json:"action"`
Payload interface{} `json:"payload"`
PayloadHash string `json:"payload_hash"`
PrevChainHash string `json:"prev_chain_hash"`
AgentPubkeyKID string `json:"agent_pubkey_kid"`
Signature string `json:"signature"`
}
// EAR is the Elydora Acknowledgment Receipt — server-issued receipt confirming acceptance.
type EAR struct {
ReceiptVersion string `json:"receipt_version"`
ReceiptID string `json:"receipt_id"`
OperationID string `json:"operation_id"`
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
ServerReceivedAt int64 `json:"server_received_at"`
SeqNo int64 `json:"seq_no"`
ChainHash string `json:"chain_hash"`
QueueMessageID string `json:"queue_message_id"`
ReceiptHash string `json:"receipt_hash"`
ElydoraKID string `json:"elydora_kid"`
ReceiptVersion string `json:"receipt_version"`
ReceiptID string `json:"receipt_id"`
OperationID string `json:"operation_id"`
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
ServerReceivedAt int64 `json:"server_received_at"`
SeqNo int64 `json:"seq_no"`
ChainHash string `json:"chain_hash"`
QueueMessageID string `json:"queue_message_id"`
ReceiptHash string `json:"receipt_hash"`
ElydoraKID string `json:"elydora_kid"`
ElydoraSignature string `json:"elydora_signature"`
}
@@ -208,6 +261,7 @@ type RegisterAgentRequest struct {
AgentID string `json:"agent_id"`
DisplayName string `json:"display_name,omitempty"`
ResponsibleEntity string `json:"responsible_entity,omitempty"`
IntegrationType IntegrationType `json:"integration_type"`
Keys []RegisterAgentKeyInput `json:"keys"`
}
@@ -326,7 +380,7 @@ type CreateExportResponse struct {
}
type GetExportResponse struct {
Export Export `json:"export"`
Export Export `json:"export"`
DownloadURL string `json:"download_url,omitempty"`
}
+92
View File
@@ -0,0 +1,92 @@
package elydora
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestRegisterAgentRequestIncludesIntegrationType(t *testing.T) {
payload, err := json.Marshal(RegisterAgentRequest{
AgentID: "agent-1",
IntegrationType: IntegrationTypeSDK,
Keys: []RegisterAgentKeyInput{},
})
if err != nil {
t.Fatalf("marshal register agent request: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("unmarshal register agent request: %v", err)
}
if got := decoded["integration_type"]; got != "sdk" {
t.Fatalf("integration_type = %v, want sdk", got)
}
}
func TestIntegrationTypesMatchPublicAPIContract(t *testing.T) {
got := []IntegrationType{
IntegrationTypeAugment,
IntegrationTypeClaudecode,
IntegrationTypeCline,
IntegrationTypeCodex,
IntegrationTypeCopilot,
IntegrationTypeCursor,
IntegrationTypeDroid,
IntegrationTypeGemini,
IntegrationTypeGrok,
IntegrationTypeKimi,
IntegrationTypeKiroCLI,
IntegrationTypeKiroIDE,
IntegrationTypeLetta,
IntegrationTypeOpenCode,
IntegrationTypeQwen,
IntegrationTypeEnterprise,
IntegrationTypeGUI,
IntegrationTypeSDK,
IntegrationTypeOther,
}
want := []IntegrationType{
"augment", "claudecode", "cline", "codex", "copilot", "cursor", "droid",
"gemini", "grok", "kimi", "kirocli", "kiroide", "letta", "opencode", "qwen",
"enterprise", "gui", "sdk", "other",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("integration types = %v, want %v", got, want)
}
for _, integrationType := range got {
if !integrationType.IsValid() {
t.Fatalf("integration type %q must be valid", integrationType)
}
}
if IntegrationType("future-cli").IsValid() {
t.Fatal("unknown integration type must be invalid")
}
}
func TestRegisterAgentRejectsInvalidIntegrationBeforeNetworkAccess(t *testing.T) {
requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
requestCount++
}))
t.Cleanup(server.Close)
client := &Client{baseURL: server.URL, httpClient: server.Client()}
requests := []*RegisterAgentRequest{
nil,
{AgentID: "agent-1"},
{AgentID: "agent-1", IntegrationType: IntegrationType("future-cli")},
}
for _, request := range requests {
if _, err := client.RegisterAgent(request); err == nil {
t.Fatalf("RegisterAgent(%+v) succeeded, want validation error", request)
}
}
if requestCount != 0 {
t.Fatalf("network request count = %d, want 0", requestCount)
}
}
+1
View File
@@ -159,6 +159,7 @@ const result = await client.verifyOperation(operationId);
// Register a new agent
const agent = await client.registerAgent({
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' }],
+2
View File
@@ -28,6 +28,7 @@ import type {
ErrorResponse,
} from './types.js';
import { ElydoraError } from './errors.js';
import { assertIntegrationType } from './integration-types.js';
import {
jcsCanonicalise,
computePayloadHash,
@@ -148,6 +149,7 @@ export class ElydoraClient {
// -------------------------------------------------------------------------
async registerAgent(request: RegisterAgentRequest): Promise<RegisterAgentResponse> {
assertIntegrationType(request?.integration_type);
return this.request<RegisterAgentResponse>('POST', '/v1/agents/register', request);
}
+2
View File
@@ -1,5 +1,6 @@
export { ElydoraClient } from './client.js';
export { ElydoraError } from './errors.js';
export { INTEGRATION_TYPES } from './types.js';
export {
jcsCanonicalise,
@@ -24,6 +25,7 @@ export type {
KeyStatus,
ExportStatus,
RbacRole,
IntegrationType,
ErrorCode,
// Entities
+11
View File
@@ -0,0 +1,11 @@
import { INTEGRATION_TYPES, type IntegrationType } from './types.js';
export function assertIntegrationType(value: unknown): asserts value is IntegrationType {
const isSupported = typeof value === 'string'
&& INTEGRATION_TYPES.some((integrationType) => integrationType === value);
if (!isSupported) {
throw new TypeError(
`Invalid integration_type "${String(value)}". Expected one of: ${INTEGRATION_TYPES.join(', ')}`,
);
}
}
+24
View File
@@ -11,6 +11,28 @@ export type RbacRole =
| 'compliance_auditor'
| 'readonly_investigator'
| 'integration_engineer';
export const INTEGRATION_TYPES = [
'augment',
'claudecode',
'cline',
'codex',
'copilot',
'cursor',
'droid',
'gemini',
'grok',
'kimi',
'kirocli',
'kiroide',
'letta',
'opencode',
'qwen',
'enterprise',
'gui',
'sdk',
'other',
] as const;
export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
export type ErrorCode =
| 'INVALID_SIGNATURE'
| 'UNKNOWN_AGENT'
@@ -36,6 +58,7 @@ export interface Agent {
readonly org_id: string;
readonly display_name: string;
readonly responsible_entity: string;
readonly integration_type: IntegrationType;
readonly status: AgentStatus;
readonly created_at: number;
readonly updated_at: number;
@@ -160,6 +183,7 @@ export interface EAR {
export interface RegisterAgentRequest {
readonly agent_id: string;
readonly integration_type: IntegrationType;
readonly display_name?: string;
readonly responsible_entity?: string;
readonly keys: ReadonlyArray<{
@@ -0,0 +1,131 @@
import assert from 'node:assert/strict';
import http from 'node:http';
import test from 'node:test';
import { ElydoraClient, INTEGRATION_TYPES } from '../dist/index.js';
const EXPECTED_INTEGRATION_TYPES = [
'augment',
'claudecode',
'cline',
'codex',
'copilot',
'cursor',
'droid',
'gemini',
'grok',
'kimi',
'kirocli',
'kiroide',
'letta',
'opencode',
'qwen',
'enterprise',
'gui',
'sdk',
'other',
];
async function createFixture() {
const requests = [];
const server = http.createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
requests.push({
method: request.method,
path: request.url,
authorization: request.headers.authorization,
body,
});
response.writeHead(201, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({
agent: {
agent_id: body.agent_id,
org_id: 'org-1',
display_name: body.display_name,
responsible_entity: body.responsible_entity,
integration_type: body.integration_type,
status: 'active',
created_at: 1,
updated_at: 1,
},
keys: [],
}));
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
assert(address && typeof address === 'object');
const client = new ElydoraClient({
orgId: 'org-1',
agentId: 'admin-agent',
privateKey: 'unused',
baseUrl: `http://127.0.0.1:${address.port}`,
maxRetries: 0,
});
client.setToken('api-token');
return {
client,
requests,
async close() {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
test('integration types match the public API contract', () => {
assert.deepEqual(INTEGRATION_TYPES, EXPECTED_INTEGRATION_TYPES);
});
test('agent registration sends integration_type in one authenticated request', async () => {
const fixture = await createFixture();
try {
await assert.rejects(
fixture.client.registerAgent({ agent_id: 'agent-1', keys: [] }),
{
name: 'TypeError',
message: /Invalid integration_type "undefined"/,
},
);
await assert.rejects(
fixture.client.registerAgent({
agent_id: 'agent-1',
integration_type: 'future-cli',
keys: [],
}),
{
name: 'TypeError',
message: /Invalid integration_type "future-cli"/,
},
);
assert.equal(fixture.requests.length, 0);
const response = await fixture.client.registerAgent({
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' }],
});
assert.equal(response.agent.integration_type, 'grok');
assert.deepEqual(fixture.requests, [{
method: 'POST',
path: '/v1/agents/register',
authorization: 'Bearer api-token',
body: {
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' }],
},
}]);
} finally {
await fixture.close();
}
});
+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()