mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 02:15:57 +00:00
Rewrite inline comments added during the agent identity hardening so they explain the code as it stands today, rather than tying to internal plan terminology (phase numbers, FR/NFR/H/M/C codes, PR references, trap zone labels). Commit history already carries the plan archaeology. Comments now keep the non-obvious invariants (cache boundaries, bypass gaps, silent-nil traps, dual-tenant semantics) and drop the scaffolding. Comment-only — no runtime behavior change.
192 lines
6.3 KiB
Go
192 lines
6.3 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// --- Add Member ---
|
|
|
|
type teamsAddMemberParams struct {
|
|
TeamID string `json:"teamId"`
|
|
Agent string `json:"agent"` // agent key or UUID
|
|
Role string `json:"role"` // "member" (default) or "reviewer"
|
|
}
|
|
|
|
func (m *TeamsMethods) handleAddMember(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
if m.teamStore == nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
|
return
|
|
}
|
|
|
|
var params teamsAddMemberParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
|
return
|
|
}
|
|
if params.TeamID == "" || params.Agent == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId and agent")))
|
|
return
|
|
}
|
|
|
|
teamID, err := uuid.Parse(params.TeamID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
|
return
|
|
}
|
|
|
|
// Validate team exists
|
|
team, err := m.teamStore.GetTeam(ctx, teamID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgNotFound, "team", err.Error())))
|
|
return
|
|
}
|
|
|
|
// Resolve agent — accepts agent_key or UUID. Return an i18n error on
|
|
// failure; never leak the raw store error string to WS clients.
|
|
ag, err := resolveAgentInfo(ctx, m.agentStore, params.Agent)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "agent")))
|
|
return
|
|
}
|
|
|
|
// Prevent adding lead again
|
|
if ag.ID == team.LeadAgentID {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAgentIsTeamLead)))
|
|
return
|
|
}
|
|
|
|
// Validate and default role
|
|
role := params.Role
|
|
switch role {
|
|
case store.TeamRoleMember, store.TeamRoleReviewer:
|
|
// valid
|
|
case "":
|
|
role = store.TeamRoleMember
|
|
default:
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "role must be member or reviewer"))
|
|
return
|
|
}
|
|
|
|
// Add member
|
|
if err := m.teamStore.AddMember(ctx, teamID, ag.ID, role); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "member", err.Error())))
|
|
return
|
|
}
|
|
|
|
// Invalidate caches for all team members
|
|
m.invalidateTeamCaches(ctx, teamID)
|
|
emitAudit(m.eventBus, client, "team.member_added", "team", teamID.String())
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
|
|
|
// Emit team.member.added event
|
|
if m.msgBus != nil {
|
|
m.msgBus.Broadcast(bus.Event{
|
|
Name: protocol.EventTeamMemberAdded,
|
|
Payload: protocol.TeamMemberAddedPayload{
|
|
TeamID: teamID.String(),
|
|
TeamName: team.Name,
|
|
AgentID: ag.ID.String(),
|
|
AgentKey: ag.AgentKey,
|
|
DisplayName: ag.DisplayName,
|
|
Role: role,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- Remove Member ---
|
|
|
|
type teamsRemoveMemberParams struct {
|
|
TeamID string `json:"teamId"`
|
|
AgentID string `json:"agentId"` // agent UUID
|
|
}
|
|
|
|
func (m *TeamsMethods) handleRemoveMember(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
if m.teamStore == nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
|
return
|
|
}
|
|
|
|
var params teamsRemoveMemberParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
|
return
|
|
}
|
|
if params.TeamID == "" || params.AgentID == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId and agentId")))
|
|
return
|
|
}
|
|
|
|
teamID, err := uuid.Parse(params.TeamID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
|
return
|
|
}
|
|
// Accept agent_key or UUID via cache-aware resolver.
|
|
agentID, err := resolveAgentUUIDCached(ctx, m.agentRouter, m.agentStore, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "agentId")))
|
|
return
|
|
}
|
|
|
|
// Validate team exists and prevent removing the lead
|
|
team, err := m.teamStore.GetTeam(ctx, teamID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgNotFound, "team", err.Error())))
|
|
return
|
|
}
|
|
if agentID == team.LeadAgentID {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCannotRemoveTeamLead)))
|
|
return
|
|
}
|
|
|
|
// Fetch agent info before removal for event emission
|
|
removedAgent, _ := m.agentStore.GetByID(ctx, agentID)
|
|
|
|
// Remove member
|
|
if err := m.teamStore.RemoveMember(ctx, teamID, agentID); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToDelete, "member", err.Error())))
|
|
return
|
|
}
|
|
|
|
// Invalidate caches for all remaining members + removed agent
|
|
m.invalidateTeamCaches(ctx, teamID)
|
|
if m.agentRouter != nil {
|
|
ag, err := m.agentStore.GetByID(ctx, agentID)
|
|
if err == nil {
|
|
m.agentRouter.InvalidateAgent(ag.AgentKey)
|
|
}
|
|
}
|
|
|
|
emitAudit(m.eventBus, client, "team.member_removed", "team", teamID.String())
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
|
|
|
// Emit team.member.removed event
|
|
if m.msgBus != nil && removedAgent != nil {
|
|
m.msgBus.Broadcast(bus.Event{
|
|
Name: protocol.EventTeamMemberRemoved,
|
|
Payload: protocol.TeamMemberRemovedPayload{
|
|
TeamID: teamID.String(),
|
|
TeamName: team.Name,
|
|
AgentID: removedAgent.ID.String(),
|
|
AgentKey: removedAgent.AgentKey,
|
|
DisplayName: removedAgent.DisplayName,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|