Skip to content

Commit 2519e72

Browse files
gcmsgclaude
andcommitted
feat(console): simplify registration, add discover page, auto-whitelist
- Change claim token default protocols from "a2a" to "a2a,mcp,acp" - Auto-whitelist: mutually add contacts for all agents owned by same user on claim - Add console directory endpoint (GET /provider/directory) that includes user's private agents - Add IncludeOwnerUserID filter to SQLite/Postgres stores - Delete RegisterWizard and AgentRegisterPage (5-step wizard removed) - Rewrite AgentEditPage as simple single-page form - Dashboard: only show ClaimTokenSection when user has no agents - ProviderAgentsPage: inline expandable ClaimTokenSection for registration - Sidebar: replace "Register Agent" with "Discover" link - New DiscoverAgentsPage with search, filters, and "Request Access" button - Add discover-related i18n keys to all 8 locales Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 96ba954 commit 2519e72

24 files changed

+908
-608
lines changed

internal/registry/postgres.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,12 @@ func (s *PostgresStore) List(ctx context.Context, filter ListFilter) (*ListResul
170170
if filter.PlaygroundOnly {
171171
conditions = append(conditions, "COALESCE(playground_enabled, FALSE) = TRUE")
172172
}
173-
if filter.PublicOnly {
173+
if filter.IncludeOwnerUserID != "" {
174+
// Show public agents + this user's own agents (including private ones).
175+
conditions = append(conditions, fmt.Sprintf("(COALESCE(visibility, 'public') = 'public' OR owner_user_id = $%d)", argIdx))
176+
args = append(args, filter.IncludeOwnerUserID)
177+
argIdx++
178+
} else if filter.PublicOnly {
174179
conditions = append(conditions, "COALESCE(visibility, 'public') = 'public'")
175180
}
176181

internal/registry/sqlite.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,11 @@ func (s *SQLiteStore) List(ctx context.Context, filter ListFilter) (*ListResult,
190190
if filter.PlaygroundOnly {
191191
conditions = append(conditions, "playground_enabled = 1")
192192
}
193-
if filter.PublicOnly {
193+
if filter.IncludeOwnerUserID != "" {
194+
// Show public agents + this user's own agents (including private ones).
195+
conditions = append(conditions, "(COALESCE(visibility, 'public') = 'public' OR owner_user_id = ?)")
196+
args = append(args, filter.IncludeOwnerUserID)
197+
} else if filter.PublicOnly {
194198
conditions = append(conditions, "COALESCE(visibility, 'public') = 'public'")
195199
}
196200

internal/registry/store.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ type ListFilter struct {
2020
SortBy string // "reputation", "name", "registered_at"
2121
OwnerUserID string // Filter by owner user ID.
2222
Category string // Filter by category slug.
23-
PlaygroundOnly bool // Only return agents with playground_enabled=true
24-
PublicOnly bool // Only return agents with visibility='public'
23+
PlaygroundOnly bool // Only return agents with playground_enabled=true
24+
PublicOnly bool // Only return agents with visibility='public'
25+
IncludeOwnerUserID string // When PublicOnly is false, also include agents owned by this user ID
2526
}
2627

2728
// ListResult holds a page of agents and pagination info.

internal/server/claim_handler.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (s *HTTPServer) handleGenerateClaimToken(w http.ResponseWriter, r *http.Req
4747
Protocols: strings.Join(req.Protocols, ","),
4848
}
4949
if params.Protocols == "" {
50-
params.Protocols = "a2a"
50+
params.Protocols = "a2a,mcp,acp"
5151
}
5252

5353
token, err := s.claimToken.Generate(r.Context(), userID, params)
@@ -151,7 +151,7 @@ func (s *HTTPServer) handleClaimAgent(w http.ResponseWriter, r *http.Request) {
151151
protoList = strings.Split(ct.Protocols, ",")
152152
}
153153
if len(protoList) == 0 {
154-
protoList = []string{"a2a"}
154+
protoList = []string{"a2a", "mcp", "acp"}
155155
}
156156

157157
caps := req.Capabilities
@@ -189,7 +189,29 @@ func (s *HTTPServer) handleClaimAgent(w http.ResponseWriter, r *http.Request) {
189189
// Agent is registered; don't fail the response.
190190
}
191191

192-
// 5. Update routing table.
192+
// 5. Auto-whitelist: mutually add all agents owned by the same user.
193+
if s.contacts != nil && ct.UserID != "" {
194+
ownedResult, err := s.registry.ListAgents(r.Context(), registry.ListFilter{
195+
OwnerUserID: ct.UserID,
196+
PageSize: 100,
197+
})
198+
if err == nil {
199+
for _, sibling := range ownedResult.Agents {
200+
if sibling.ID == card.ID {
201+
continue
202+
}
203+
// Mutual: new→sibling + sibling→new
204+
if _, err := s.contacts.Add(r.Context(), card.ID, sibling.ID, "", nil); err != nil {
205+
s.logger.Debug("auto-whitelist: failed to add contact", "from", card.ID, "to", sibling.ID, "error", err)
206+
}
207+
if _, err := s.contacts.Add(r.Context(), sibling.ID, card.ID, "", nil); err != nil {
208+
s.logger.Debug("auto-whitelist: failed to add contact", "from", sibling.ID, "to", card.ID, "error", err)
209+
}
210+
}
211+
}
212+
}
213+
214+
// 6. Update routing table.
193215
s.engine.UpdateFromCard(card)
194216

195217
// Record reputation event.

internal/server/http.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,7 @@ func (s *HTTPServer) registerProviderRoutes() {
751751
s.mux.Handle("DELETE /api/v1/provider/agents/{id}", s.wrapUserAuth(s.handleProviderDeleteAgent))
752752
s.mux.Handle("GET /api/v1/provider/agents/{id}/analytics", s.wrapUserAuth(s.handleProviderAgentAnalytics))
753753
s.mux.Handle("GET /api/v1/provider/dashboard", s.wrapUserAuth(s.handleProviderDashboard))
754+
s.mux.Handle("GET /api/v1/provider/directory", s.wrapUserAuth(s.handleConsoleDirectory))
754755
}
755756

756757
func (s *HTTPServer) registerClaimTokenRoutes() {

internal/server/provider_handler.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package server
33
import (
44
"encoding/json"
55
"net/http"
6+
"sort"
67
"strconv"
78
"time"
89

@@ -496,3 +497,116 @@ func (s *HTTPServer) handleProviderDashboard(w http.ResponseWriter, r *http.Requ
496497
"agents": agents,
497498
})
498499
}
500+
501+
// handleConsoleDirectory handles GET /api/v1/provider/directory — console directory search.
502+
// Unlike the public directory, this includes the user's own private agents.
503+
func (s *HTTPServer) handleConsoleDirectory(w http.ResponseWriter, r *http.Request) {
504+
userID, ok := identity.UserIDFromContext(r.Context())
505+
if !ok {
506+
s.jsonError(w, "unauthorized", http.StatusUnauthorized)
507+
return
508+
}
509+
510+
q := r.URL.Query()
511+
512+
filter := registry.ListFilter{
513+
Protocol: q.Get("protocol"),
514+
Capability: q.Get("capability"),
515+
Search: q.Get("search"),
516+
PageToken: q.Get("page_token"),
517+
SortBy: q.Get("sort"),
518+
IncludeOwnerUserID: userID,
519+
}
520+
521+
if q.Get("status") != "" {
522+
filter.Status = agentcard.AgentStatus(q.Get("status"))
523+
}
524+
if q.Get("verified") == "true" {
525+
filter.Verified = true
526+
}
527+
if ms := q.Get("min_score"); ms != "" {
528+
if score, err := strconv.ParseFloat(ms, 64); err == nil {
529+
filter.MinScore = score
530+
}
531+
}
532+
if ps := q.Get("page_size"); ps != "" {
533+
if size, err := strconv.Atoi(ps); err == nil {
534+
filter.PageSize = size
535+
}
536+
}
537+
if q.Get("category") != "" {
538+
filter.Category = q.Get("category")
539+
}
540+
if q.Get("playground_only") == "true" {
541+
filter.PlaygroundOnly = true
542+
}
543+
544+
sortByPopular := filter.SortBy == "popular"
545+
if filter.SortBy == "" {
546+
filter.SortBy = "reputation"
547+
}
548+
if sortByPopular {
549+
filter.SortBy = "reputation"
550+
}
551+
552+
result, err := s.registry.ListAgents(r.Context(), filter)
553+
if err != nil {
554+
s.jsonError(w, err.Error(), http.StatusInternalServerError)
555+
return
556+
}
557+
558+
// Build call counts for popular sort.
559+
callCounts := make(map[string]int64)
560+
if sortByPopular && s.invocation != nil {
561+
since := time.Now().AddDate(0, 0, -7)
562+
topAgents, err := s.invocation.TopAgents(r.Context(), since, 200)
563+
if err == nil {
564+
for _, a := range topAgents {
565+
callCounts[a.AgentID] = a.TotalCalls
566+
}
567+
}
568+
}
569+
570+
// Batch-fetch access flags and reputation.
571+
agentIDs := make([]string, len(result.Agents))
572+
for i, card := range result.Agents {
573+
agentIDs[i] = card.ID
574+
}
575+
576+
flagsMap, _ := s.registry.GetAccessFlagsBatch(r.Context(), agentIDs)
577+
if flagsMap == nil {
578+
flagsMap = map[string]*registry.AccessFlags{}
579+
}
580+
581+
var scoresMap map[string]float64
582+
if s.reputation != nil {
583+
scoresMap = s.reputation.GetScoresBatch(r.Context(), agentIDs)
584+
}
585+
586+
profiles := make([]PublicAgentProfile, 0, len(result.Agents))
587+
for _, card := range result.Agents {
588+
p := toPublicProfile(card)
589+
if flags, ok := flagsMap[card.ID]; ok && flags != nil {
590+
p.PlaygroundEnabled = flags.PlaygroundEnabled
591+
}
592+
if score, ok := scoresMap[card.ID]; ok {
593+
p.ReputationScore = score
594+
}
595+
if count, ok := callCounts[card.ID]; ok {
596+
p.TotalCalls = count
597+
}
598+
profiles = append(profiles, p)
599+
}
600+
601+
if sortByPopular {
602+
sort.Slice(profiles, func(i, j int) bool {
603+
return profiles[i].TotalCalls > profiles[j].TotalCalls
604+
})
605+
}
606+
607+
s.jsonResponse(w, http.StatusOK, DirectoryResponse{
608+
Agents: profiles,
609+
NextPageToken: result.NextPageToken,
610+
TotalCount: result.TotalCount,
611+
})
612+
}

web/app/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { PlaygroundPage } from "@/pages/PlaygroundPage"
1919
import { AboutPage } from "@/pages/AboutPage"
2020
import { ProviderDashboardPage } from "@/pages/ProviderDashboardPage"
2121
import { ProviderAgentsPage } from "@/pages/ProviderAgentsPage"
22-
import { AgentRegisterPage } from "@/pages/AgentRegisterPage"
22+
import { DiscoverAgentsPage } from "@/pages/DiscoverAgentsPage"
2323
import { ProviderAgentDetailPage } from "@/pages/ProviderAgentDetailPage"
2424
import { AgentEditPage } from "@/pages/AgentEditPage"
2525
import { InvocationHistoryPage } from "@/pages/InvocationHistoryPage"
@@ -61,7 +61,7 @@ export function App() {
6161
}
6262
>
6363
<Route index element={<ProviderDashboardPage />} />
64-
<Route path="register" element={<AgentRegisterPage />} />
64+
<Route path="discover" element={<DiscoverAgentsPage />} />
6565
<Route path="agents" element={<ProviderAgentsPage />} />
6666
<Route path="agents/:id" element={<ProviderAgentDetailPage />} />
6767
<Route path="agents/:id/edit" element={<AgentEditPage />} />

web/app/src/api/client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,30 @@ export function fetchDirectory(
8383
return fetchJSON<DirectoryResponse>(`/directory${qs ? `?${qs}` : ""}`)
8484
}
8585

86+
// Console directory API (includes user's own private agents)
87+
export function fetchConsoleDirectory(
88+
params: DirectoryParams | undefined,
89+
accessToken: string
90+
): Promise<DirectoryResponse> {
91+
const query = new URLSearchParams()
92+
if (params?.capability) query.set("capability", params.capability)
93+
if (params?.protocol) query.set("protocol", params.protocol)
94+
if (params?.status) query.set("status", params.status)
95+
if (params?.verified) query.set("verified", "true")
96+
if (params?.min_score) query.set("min_score", String(params.min_score))
97+
if (params?.search) query.set("search", params.search)
98+
if (params?.sort) query.set("sort", params.sort)
99+
if (params?.page_size) query.set("page_size", String(params.page_size))
100+
if (params?.page_token) query.set("page_token", params.page_token)
101+
if (params?.category) query.set("category", params.category)
102+
if (params?.playground_only) query.set("playground_only", "true")
103+
const qs = query.toString()
104+
return fetchWithAuth<DirectoryResponse>(
105+
`/provider/directory${qs ? `?${qs}` : ""}`,
106+
accessToken
107+
)
108+
}
109+
86110
export function fetchPublicProfile(id: string): Promise<PublicAgentProfile> {
87111
return fetchJSON<PublicAgentProfile>(`/directory/${id}`)
88112
}

web/app/src/components/layout/ConsoleLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { UserMenu } from "@/components/layout/UserMenu"
66
import {
77
LayoutDashboard,
88
Bot,
9-
PlusCircle,
9+
Search,
1010
Activity,
1111
KeyRound,
1212
Github,
@@ -22,7 +22,7 @@ export function ConsoleLayout() {
2222
const navLinks = [
2323
{ to: "/console", label: t('nav.dashboard'), icon: LayoutDashboard, end: true },
2424
{ to: "/console/agents", label: t('nav.myAgents'), icon: Bot, end: false },
25-
{ to: "/console/register", label: t('nav.registerAgent'), icon: PlusCircle, end: false },
25+
{ to: "/console/discover", label: t('nav.discoverAgents'), icon: Search, end: false },
2626
{ to: "/console/invocations", label: t('nav.invocations'), icon: Activity, end: false },
2727
{ to: "/console/access-requests", label: t('nav.accessRequests'), icon: Lock, end: false },
2828
{ to: "/console/api-keys", label: t('nav.apiKeys'), icon: KeyRound, end: false },

web/app/src/components/provider/ClaimTokenSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function ClaimTokenSection() {
5858
try {
5959
const res = await generate({
6060
agent_name: agentName.trim(),
61-
protocols: ["a2a"],
61+
protocols: ["a2a", "mcp", "acp"],
6262
})
6363
setGeneratedCode(res.token)
6464
setExpiresAt(res.expires_at)

0 commit comments

Comments
 (0)