fix: enforce atomic agent registration contract
This commit is contained in:
1 parent
47d323c17b
commit
f64d00673c
58 files changed
+2692
-1667
No files matched your search
@@ -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,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
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user