Merge branch 'worktree-agent-a37a6029' into 007-platform-features-bundle
This commit is contained in:
+25
-1
@@ -308,7 +308,31 @@ func addAdminCommands(rootCmd *cobra.Command) {
|
||||
agentRevokeKeyCmd.Flags().StringVar(&agentRevokeKeyName, "name", "", "Agent name")
|
||||
agentRevokeKeyCmd.MarkFlagRequired("name")
|
||||
|
||||
agentCmd.AddCommand(agentListCmd, agentCreateCmd, agentDeleteCmd, agentRevokeKeyCmd)
|
||||
var (
|
||||
agentUpdateCapsName string
|
||||
agentUpdateCapsJSON string
|
||||
)
|
||||
agentUpdateCapsCmd := &cobra.Command{
|
||||
Use: "update-capabilities",
|
||||
Short: "Update an agent's capabilities JSON",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := adminRequest("agent.update_capabilities", map[string]interface{}{
|
||||
"name": agentUpdateCapsName,
|
||||
"capabilities": json.RawMessage(agentUpdateCapsJSON),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(resp["data"])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
agentUpdateCapsCmd.Flags().StringVar(&agentUpdateCapsName, "name", "", "Agent name")
|
||||
agentUpdateCapsCmd.Flags().StringVar(&agentUpdateCapsJSON, "capabilities", "", "Capabilities JSON (e.g. '{\"role\":\"researcher\"}')")
|
||||
agentUpdateCapsCmd.MarkFlagRequired("name")
|
||||
agentUpdateCapsCmd.MarkFlagRequired("capabilities")
|
||||
|
||||
agentCmd.AddCommand(agentListCmd, agentCreateCmd, agentDeleteCmd, agentRevokeKeyCmd, agentUpdateCapsCmd)
|
||||
|
||||
// ----- audit commands -----
|
||||
auditCmd := &cobra.Command{
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/synapbus/synapbus/internal/a2a"
|
||||
"github.com/synapbus/synapbus/internal/actions"
|
||||
"github.com/synapbus/synapbus/internal/admin"
|
||||
"github.com/synapbus/synapbus/internal/agents"
|
||||
@@ -528,6 +529,14 @@ func runServe(cmd *cobra.Command, args []string) error {
|
||||
// OAuth metadata (public, per RFC 8414)
|
||||
r.Get("/.well-known/oauth-authorization-server", authHandlers.HandleOAuthMetadata)
|
||||
|
||||
// A2A Agent Card discovery (public, no auth required)
|
||||
agentCardBaseURL := authCfg.IssuerURL // reuse the same base URL config
|
||||
r.Get("/.well-known/agent-card.json", a2a.NewAgentCardHandler(
|
||||
&a2aAgentListerAdapter{agentService: agentService},
|
||||
agentCardBaseURL,
|
||||
version,
|
||||
))
|
||||
|
||||
// OAuth endpoints
|
||||
r.Get("/oauth/authorize", authHandlers.HandleAuthorizeGet)
|
||||
r.Post("/oauth/authorize", authHandlers.HandleAuthorizePost)
|
||||
@@ -760,6 +769,28 @@ func generateRandomPassword() string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// a2aAgentListerAdapter adapts agents.AgentService to a2a.AgentLister.
|
||||
type a2aAgentListerAdapter struct {
|
||||
agentService *agents.AgentService
|
||||
}
|
||||
|
||||
func (a *a2aAgentListerAdapter) ListAllActiveAgents(ctx context.Context) ([]a2a.AgentInfo, error) {
|
||||
agentsList, err := a.agentService.ListAllActiveAgents(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]a2a.AgentInfo, 0, len(agentsList))
|
||||
for _, agent := range agentsList {
|
||||
result = append(result, a2a.AgentInfo{
|
||||
Name: agent.Name,
|
||||
DisplayName: agent.DisplayName,
|
||||
Type: agent.Type,
|
||||
Capabilities: agent.Capabilities,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// agentListerAdapter adapts agents.AgentService to auth.AgentLister.
|
||||
type agentListerAdapter struct {
|
||||
agentService *agents.AgentService
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Package a2a provides A2A Agent Card discovery for SynapBus.
|
||||
// The Agent Card is a JSON document that describes the hub and its registered
|
||||
// agents, following the A2A Agent Card specification.
|
||||
package a2a
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// AgentCard is the A2A Agent Card document returned by the discovery endpoint.
|
||||
type AgentCard struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
SupportedInterfaces []AgentInterface `json:"supported_interfaces"`
|
||||
Capabilities AgentCapabilities `json:"capabilities"`
|
||||
Skills []AgentSkill `json:"skills"`
|
||||
SecuritySchemes map[string]any `json:"security_schemes"`
|
||||
DefaultInputModes []string `json:"default_input_modes"`
|
||||
DefaultOutputModes []string `json:"default_output_modes"`
|
||||
}
|
||||
|
||||
// AgentInterface describes a protocol endpoint the hub supports.
|
||||
type AgentInterface struct {
|
||||
URL string `json:"url"`
|
||||
ProtocolBinding string `json:"protocol_binding"`
|
||||
}
|
||||
|
||||
// AgentCapabilities declares hub-level capabilities.
|
||||
type AgentCapabilities struct {
|
||||
Streaming bool `json:"streaming"`
|
||||
PushNotifications bool `json:"push_notifications"`
|
||||
}
|
||||
|
||||
// AgentSkill represents a single agent registered on the hub, mapped as an
|
||||
// A2A skill.
|
||||
type AgentSkill struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// AgentInfo is a lightweight struct used to pass agent data from the registry
|
||||
// into the card generator without leaking internal types.
|
||||
type AgentInfo struct {
|
||||
Name string
|
||||
DisplayName string
|
||||
Type string
|
||||
Capabilities json.RawMessage
|
||||
}
|
||||
|
||||
// GenerateAgentCard builds an AgentCard from the hub configuration and a list
|
||||
// of registered agents.
|
||||
func GenerateAgentCard(baseURL string, version string, agents []AgentInfo) *AgentCard {
|
||||
skills := make([]AgentSkill, 0, len(agents))
|
||||
for _, a := range agents {
|
||||
skill := AgentSkill{
|
||||
ID: a.Name,
|
||||
Name: a.DisplayName,
|
||||
}
|
||||
if skill.Name == "" {
|
||||
skill.Name = a.Name
|
||||
}
|
||||
|
||||
// Parse capabilities JSON for description and tags.
|
||||
if len(a.Capabilities) > 0 {
|
||||
var caps map[string]interface{}
|
||||
if json.Unmarshal(a.Capabilities, &caps) == nil {
|
||||
if desc, ok := caps["description"].(string); ok {
|
||||
skill.Description = desc
|
||||
}
|
||||
if role, ok := caps["role"].(string); ok {
|
||||
skill.Tags = append(skill.Tags, role)
|
||||
}
|
||||
if tagsRaw, ok := caps["tags"]; ok {
|
||||
switch v := tagsRaw.(type) {
|
||||
case []interface{}:
|
||||
for _, t := range v {
|
||||
if s, ok := t.(string); ok {
|
||||
skill.Tags = append(skill.Tags, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add agent type as a tag.
|
||||
if a.Type != "" {
|
||||
skill.Tags = append(skill.Tags, a.Type)
|
||||
}
|
||||
|
||||
skills = append(skills, skill)
|
||||
}
|
||||
|
||||
return &AgentCard{
|
||||
Name: "SynapBus Hub",
|
||||
Description: "MCP-native agent-to-agent messaging hub",
|
||||
Version: version,
|
||||
SupportedInterfaces: []AgentInterface{
|
||||
{
|
||||
URL: baseURL + "/a2a",
|
||||
ProtocolBinding: "JSONRPC",
|
||||
},
|
||||
},
|
||||
Capabilities: AgentCapabilities{
|
||||
Streaming: true,
|
||||
PushNotifications: false,
|
||||
},
|
||||
Skills: skills,
|
||||
SecuritySchemes: map[string]any{
|
||||
"apiKey": map[string]any{
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
"scheme": "Bearer",
|
||||
},
|
||||
"oauth2": map[string]any{
|
||||
"type": "oauth2",
|
||||
"flows": map[string]any{
|
||||
"authorizationCode": map[string]any{
|
||||
"authorizationUrl": baseURL + "/oauth/authorize",
|
||||
"tokenUrl": baseURL + "/oauth/token",
|
||||
"scopes": map[string]string{
|
||||
"mcp": "MCP protocol access",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultInputModes: []string{"text"},
|
||||
DefaultOutputModes: []string{"text"},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package a2a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateAgentCard_WithAgents(t *testing.T) {
|
||||
agents := []AgentInfo{
|
||||
{Name: "research-bot", DisplayName: "Research Bot", Type: "ai", Capabilities: json.RawMessage(`{"role":"researcher","description":"Searches the web"}`)},
|
||||
{Name: "social-commenter", DisplayName: "Social Commenter", Type: "ai", Capabilities: json.RawMessage(`{"tags":["social","marketing"]}`)},
|
||||
{Name: "data-analyst", DisplayName: "", Type: "ai", Capabilities: json.RawMessage(`{}`)},
|
||||
}
|
||||
|
||||
card := GenerateAgentCard("http://localhost:8080", "1.0.0", agents)
|
||||
|
||||
if card.Name != "SynapBus Hub" {
|
||||
t.Errorf("name = %q, want %q", card.Name, "SynapBus Hub")
|
||||
}
|
||||
if card.Version != "1.0.0" {
|
||||
t.Errorf("version = %q, want %q", card.Version, "1.0.0")
|
||||
}
|
||||
if len(card.Skills) != 3 {
|
||||
t.Fatalf("skills count = %d, want 3", len(card.Skills))
|
||||
}
|
||||
|
||||
// Verify first skill has description and tags from capabilities
|
||||
s0 := card.Skills[0]
|
||||
if s0.ID != "research-bot" {
|
||||
t.Errorf("skill[0].id = %q, want %q", s0.ID, "research-bot")
|
||||
}
|
||||
if s0.Name != "Research Bot" {
|
||||
t.Errorf("skill[0].name = %q, want %q", s0.Name, "Research Bot")
|
||||
}
|
||||
if s0.Description != "Searches the web" {
|
||||
t.Errorf("skill[0].description = %q, want %q", s0.Description, "Searches the web")
|
||||
}
|
||||
// Should have "researcher" from role + "ai" from type
|
||||
if len(s0.Tags) < 2 {
|
||||
t.Errorf("skill[0].tags = %v, expected at least 2 tags", s0.Tags)
|
||||
}
|
||||
|
||||
// Second skill should have tags from capabilities "tags" field
|
||||
s1 := card.Skills[1]
|
||||
if s1.ID != "social-commenter" {
|
||||
t.Errorf("skill[1].id = %q, want %q", s1.ID, "social-commenter")
|
||||
}
|
||||
foundSocial := false
|
||||
for _, tag := range s1.Tags {
|
||||
if tag == "social" {
|
||||
foundSocial = true
|
||||
}
|
||||
}
|
||||
if !foundSocial {
|
||||
t.Errorf("skill[1].tags = %v, expected 'social' tag", s1.Tags)
|
||||
}
|
||||
|
||||
// Third skill should use name as display name (since DisplayName is empty)
|
||||
s2 := card.Skills[2]
|
||||
if s2.Name != "data-analyst" {
|
||||
t.Errorf("skill[2].name = %q, want %q (fallback to Name)", s2.Name, "data-analyst")
|
||||
}
|
||||
|
||||
// Verify JSON serialization round-trips cleanly
|
||||
data, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal card: %v", err)
|
||||
}
|
||||
var decoded AgentCard
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal card: %v", err)
|
||||
}
|
||||
if decoded.Name != card.Name {
|
||||
t.Errorf("round-trip name mismatch")
|
||||
}
|
||||
if len(decoded.Skills) != 3 {
|
||||
t.Errorf("round-trip skills count = %d, want 3", len(decoded.Skills))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAgentCard_WithCapabilities_TagsPopulated(t *testing.T) {
|
||||
agents := []AgentInfo{
|
||||
{
|
||||
Name: "smart-agent",
|
||||
DisplayName: "Smart Agent",
|
||||
Type: "ai",
|
||||
Capabilities: json.RawMessage(`{"role":"analyst","description":"Analyzes data","tags":["ml","data"]}`),
|
||||
},
|
||||
}
|
||||
|
||||
card := GenerateAgentCard("http://example.com", "2.0.0", agents)
|
||||
|
||||
if len(card.Skills) != 1 {
|
||||
t.Fatalf("skills count = %d, want 1", len(card.Skills))
|
||||
}
|
||||
|
||||
skill := card.Skills[0]
|
||||
if skill.Description != "Analyzes data" {
|
||||
t.Errorf("description = %q, want %q", skill.Description, "Analyzes data")
|
||||
}
|
||||
|
||||
// Expect tags: "analyst" (from role), "ml", "data" (from tags), "ai" (from type)
|
||||
expectedTags := map[string]bool{"analyst": false, "ml": false, "data": false, "ai": false}
|
||||
for _, tag := range skill.Tags {
|
||||
if _, ok := expectedTags[tag]; ok {
|
||||
expectedTags[tag] = true
|
||||
}
|
||||
}
|
||||
for tag, found := range expectedTags {
|
||||
if !found {
|
||||
t.Errorf("missing expected tag %q in %v", tag, skill.Tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAgentCard_NoAgents(t *testing.T) {
|
||||
card := GenerateAgentCard("http://localhost:8080", "0.1.0", nil)
|
||||
|
||||
if card.Name != "SynapBus Hub" {
|
||||
t.Errorf("name = %q, want %q", card.Name, "SynapBus Hub")
|
||||
}
|
||||
if len(card.Skills) != 0 {
|
||||
t.Errorf("skills count = %d, want 0", len(card.Skills))
|
||||
}
|
||||
if len(card.SupportedInterfaces) != 1 {
|
||||
t.Fatalf("interfaces count = %d, want 1", len(card.SupportedInterfaces))
|
||||
}
|
||||
if card.SupportedInterfaces[0].URL != "http://localhost:8080/a2a" {
|
||||
t.Errorf("interface url = %q, want %q", card.SupportedInterfaces[0].URL, "http://localhost:8080/a2a")
|
||||
}
|
||||
if card.SecuritySchemes == nil {
|
||||
t.Error("security_schemes should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// mockAgentLister implements AgentLister for handler tests.
|
||||
type mockAgentLister struct {
|
||||
agents []AgentInfo
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockAgentLister) ListAllActiveAgents(_ context.Context) ([]AgentInfo, error) {
|
||||
return m.agents, m.err
|
||||
}
|
||||
|
||||
func TestHandler_Returns200WithCorrectContentType(t *testing.T) {
|
||||
lister := &mockAgentLister{
|
||||
agents: []AgentInfo{
|
||||
{Name: "bot-1", DisplayName: "Bot One", Type: "ai"},
|
||||
{Name: "bot-2", DisplayName: "Bot Two", Type: "ai"},
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentCardHandler(lister, "http://localhost:8080", "1.0.0")
|
||||
req := httptest.NewRequest(http.MethodGet, "/.well-known/agent-card.json", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", rr.Code, http.StatusOK)
|
||||
}
|
||||
ct := rr.Header().Get("Content-Type")
|
||||
if ct != "application/json" {
|
||||
t.Errorf("Content-Type = %q, want %q", ct, "application/json")
|
||||
}
|
||||
|
||||
var card AgentCard
|
||||
if err := json.NewDecoder(rr.Body).Decode(&card); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if card.Name != "SynapBus Hub" {
|
||||
t.Errorf("card.name = %q, want %q", card.Name, "SynapBus Hub")
|
||||
}
|
||||
if len(card.Skills) != 2 {
|
||||
t.Errorf("card.skills count = %d, want 2", len(card.Skills))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_DerivesBaseURLFromRequest(t *testing.T) {
|
||||
lister := &mockAgentLister{agents: nil}
|
||||
|
||||
// Empty configuredBaseURL — should derive from request
|
||||
handler := NewAgentCardHandler(lister, "", "1.0.0")
|
||||
req := httptest.NewRequest(http.MethodGet, "/.well-known/agent-card.json", nil)
|
||||
req.Host = "myhost:9090"
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var card AgentCard
|
||||
if err := json.NewDecoder(rr.Body).Decode(&card); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if card.SupportedInterfaces[0].URL != "http://myhost:9090/a2a" {
|
||||
t.Errorf("interface url = %q, want %q", card.SupportedInterfaces[0].URL, "http://myhost:9090/a2a")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_RejectsNonGET(t *testing.T) {
|
||||
lister := &mockAgentLister{agents: nil}
|
||||
handler := NewAgentCardHandler(lister, "http://localhost:8080", "1.0.0")
|
||||
req := httptest.NewRequest(http.MethodPost, "/.well-known/agent-card.json", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("status = %d, want %d", rr.Code, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package a2a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AgentLister abstracts the operation of listing active non-human agents.
|
||||
type AgentLister interface {
|
||||
ListAllActiveAgents(ctx context.Context) ([]AgentInfo, error)
|
||||
}
|
||||
|
||||
// NewAgentCardHandler returns an http.HandlerFunc that serves the A2A Agent
|
||||
// Card JSON document at /.well-known/agent-card.json.
|
||||
//
|
||||
// The handler is public (no auth required) because Agent Cards are meant for
|
||||
// discovery. If configuredBaseURL is empty the base URL is derived from the
|
||||
// incoming request (respecting X-Forwarded-* headers).
|
||||
func NewAgentCardHandler(agentLister AgentLister, configuredBaseURL string, version string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Derive base URL from request if not configured.
|
||||
baseURL := configuredBaseURL
|
||||
if baseURL == "" {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
||||
scheme = proto
|
||||
}
|
||||
host := r.Host
|
||||
if fwdHost := r.Header.Get("X-Forwarded-Host"); fwdHost != "" {
|
||||
host = fwdHost
|
||||
}
|
||||
baseURL = scheme + "://" + host
|
||||
}
|
||||
|
||||
agents, err := agentLister.ListAllActiveAgents(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
card := GenerateAgentCard(baseURL, version, agents)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=60")
|
||||
json.NewEncoder(w).Encode(card)
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,8 @@ func (s *AdminServer) dispatch(req Request) Response {
|
||||
return s.handleAgentDelete(ctx, req.Args)
|
||||
case "agent.revoke_key":
|
||||
return s.handleAgentRevokeKey(ctx, req.Args)
|
||||
case "agent.update_capabilities":
|
||||
return s.handleAgentUpdateCapabilities(ctx, req.Args)
|
||||
|
||||
// --- audit commands ---
|
||||
case "audit.list":
|
||||
@@ -446,6 +448,35 @@ func (s *AdminServer) handleAgentRevokeKey(ctx context.Context, args json.RawMes
|
||||
}}
|
||||
}
|
||||
|
||||
func (s *AdminServer) handleAgentUpdateCapabilities(ctx context.Context, args json.RawMessage) Response {
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
Capabilities json.RawMessage `json:"capabilities"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &p); err != nil {
|
||||
return Response{OK: false, Error: "invalid args: " + err.Error()}
|
||||
}
|
||||
if p.Name == "" {
|
||||
return Response{OK: false, Error: "name is required"}
|
||||
}
|
||||
if len(p.Capabilities) == 0 {
|
||||
return Response{OK: false, Error: "capabilities is required"}
|
||||
}
|
||||
if !json.Valid(p.Capabilities) {
|
||||
return Response{OK: false, Error: "capabilities must be valid JSON"}
|
||||
}
|
||||
|
||||
agent, err := s.services.Agents.UpdateAgent(ctx, p.Name, "", p.Capabilities)
|
||||
if err != nil {
|
||||
return Response{OK: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
return Response{OK: true, Data: map[string]interface{}{
|
||||
"name": agent.Name,
|
||||
"capabilities": json.RawMessage(agent.Capabilities),
|
||||
}}
|
||||
}
|
||||
|
||||
// ---------- audit handlers ----------
|
||||
|
||||
func (s *AdminServer) handleAuditList(ctx context.Context, args json.RawMessage) Response {
|
||||
|
||||
@@ -245,6 +245,12 @@ func (s *AgentService) ListAgents(ctx context.Context, ownerID int64) ([]*Agent,
|
||||
return s.store.ListAgentsByOwner(ctx, ownerID)
|
||||
}
|
||||
|
||||
// ListAllActiveAgents returns all active non-human agents across all owners.
|
||||
// Used for the A2A Agent Card discovery endpoint.
|
||||
func (s *AgentService) ListAllActiveAgents(ctx context.Context) ([]*Agent, error) {
|
||||
return s.store.ListAllActiveAgents(ctx)
|
||||
}
|
||||
|
||||
// RevokeKey generates a new API key for an agent. Only the owner can do this.
|
||||
// Returns the agent and the new raw API key (shown once).
|
||||
func (s *AgentService) RevokeKey(ctx context.Context, name string, ownerID int64) (*Agent, string, error) {
|
||||
|
||||
@@ -15,6 +15,7 @@ type AgentStore interface {
|
||||
UpdateAgent(ctx context.Context, agent *Agent) error
|
||||
DeactivateAgent(ctx context.Context, name string) error
|
||||
ListActiveAgents(ctx context.Context) ([]*Agent, error)
|
||||
ListAllActiveAgents(ctx context.Context) ([]*Agent, error)
|
||||
ListAgentsByOwner(ctx context.Context, ownerID int64) ([]*Agent, error)
|
||||
SearchAgentsByCapability(ctx context.Context, query string) ([]*Agent, error)
|
||||
GetHumanAgentByOwner(ctx context.Context, ownerID int64) (*Agent, error)
|
||||
@@ -112,6 +113,18 @@ func (s *SQLiteAgentStore) ListActiveAgents(ctx context.Context) ([]*Agent, erro
|
||||
return s.scanAgents(rows)
|
||||
}
|
||||
|
||||
func (s *SQLiteAgentStore) ListAllActiveAgents(ctx context.Context) ([]*Agent, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT id, name, display_name, type, capabilities, owner_id, api_key_hash, status, created_at, updated_at
|
||||
FROM agents WHERE status = 'active' AND type != 'human' ORDER BY name`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return s.scanAgents(rows)
|
||||
}
|
||||
|
||||
func (s *SQLiteAgentStore) ListAgentsByOwner(ctx context.Context, ownerID int64) ([]*Agent, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT id, name, display_name, type, capabilities, owner_id, api_key_hash, status, created_at, updated_at
|
||||
|
||||
Reference in New Issue
Block a user