package agentstore import ( "context" "crypto/rand" "database/sql" "encoding/hex" "errors" "fmt" "strings" "sync" "time" "golang.org/x/crypto/bcrypt" "aijianzhan/platform/internal/authx" ) const ( StatusActive = "active" StatusPending = "pending" StatusDisabled = "disabled" ) type Account struct { AgentID int64 `json:"agent_id"` TenantID int64 `json:"tenant_id"` Name string `json:"name"` ClientID string `json:"client_id"` HostKey string `json:"host_key,omitempty"` RoleID int64 `json:"role_id,omitempty"` RoleCode string `json:"role_code,omitempty"` RoleName string `json:"role_name,omitempty"` Status string `json:"status"` Perms []string `json:"permissions"` AppSlugs []string `json:"app_slugs"` // Z8b:智能体绑定的同步通道 / 线上库(模块与松离线同一目标) ChannelID string `json:"channel_id,omitempty"` OnlineDBID string `json:"online_db_id,omitempty"` DatabaseName string `json:"database_name,omitempty"` CreatedBy int64 `json:"created_by"` CreatedAt time.Time `json:"created_at"` LastTokenAt *time.Time `json:"last_token_at,omitempty"` } type CreateInput struct { Name string Perms []string AppSlugs []string Status string // 空则 active HostKey string RoleID int64 ChannelID string OnlineDBID string DatabaseName string } type UpdateInput struct { Name *string Status *string Perms *[]string AppSlugs *[]string RoleID *int64 ChannelID *string OnlineDBID *string DatabaseName *string } type Store interface { List(ctx context.Context, tenantID int64) ([]Account, error) Get(ctx context.Context, tenantID, agentID int64) (*Account, error) Create(ctx context.Context, tenantID, createdBy int64, in CreateInput) (acc *Account, clientSecret string, err error) Register(ctx context.Context, tenantID int64, name, hostKey string) (acc *Account, clientSecret string, reused bool, err error) Update(ctx context.Context, tenantID, agentID int64, in UpdateInput) (*Account, error) RotateSecret(ctx context.Context, tenantID, agentID int64) (clientSecret string, err error) Delete(ctx context.Context, tenantID, agentID int64) error Authenticate(ctx context.Context, clientID, clientSecret string) (*Account, error) TouchToken(ctx context.Context, agentID int64) error HasAppAccess(ctx context.Context, agentID int64, slug string) (bool, error) // GrantAppSlug 将 slug 写入智能体可访问模块(幂等)。新建发布时自动授权用。 GrantAppSlug(ctx context.Context, agentID int64, slug string) error // FindByHostKey 按宿主机 key 查找(跨租户,Z13 绑定用)。 FindByHostKey(ctx context.Context, hostKey string) (*Account, error) // AttachSyncBind 写入同步落点;可改挂租户并激活(Z12/Z13)。 AttachSyncBind(ctx context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) } type memAcc struct { Account SecretHash string } type MemoryStore struct { mu sync.Mutex seq int64 byID map[int64]*memAcc byCID map[string]*memAcc } func NewMemoryStore() *MemoryStore { return &MemoryStore{byID: map[int64]*memAcc{}, byCID: map[string]*memAcc{}} } func (s *MemoryStore) List(_ context.Context, tenantID int64) ([]Account, error) { s.mu.Lock() defer s.mu.Unlock() out := []Account{} for _, a := range s.byID { if a.TenantID == tenantID { out = append(out, cloneAcc(&a.Account)) } } return out, nil } func (s *MemoryStore) Get(_ context.Context, tenantID, agentID int64) (*Account, error) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok || a.TenantID != tenantID { return nil, fmt.Errorf("agent not found") } cp := cloneAcc(&a.Account) return &cp, nil } func (s *MemoryStore) Create(_ context.Context, tenantID, createdBy int64, in CreateInput) (*Account, string, error) { s.mu.Lock() defer s.mu.Unlock() return s.createLocked(tenantID, createdBy, in) } func (s *MemoryStore) Register(_ context.Context, tenantID int64, name, hostKey string) (*Account, string, bool, error) { s.mu.Lock() defer s.mu.Unlock() hostKey = strings.TrimSpace(hostKey) if hostKey != "" { for _, a := range s.byID { if a.TenantID == tenantID && a.HostKey == hostKey { if a.Status == StatusPending { secret := randomHex(24) hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) if err != nil { return nil, "", false, err } a.SecretHash = string(hash) if n := strings.TrimSpace(name); n != "" { a.Name = n } cp := cloneAcc(&a.Account) return &cp, secret, true, nil } return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", a.ClientID, a.Status) } } } acc, secret, err := s.createLocked(tenantID, 0, CreateInput{ Name: name, Status: StatusPending, HostKey: hostKey, }) return acc, secret, false, err } func (s *MemoryStore) createLocked(tenantID, createdBy int64, in CreateInput) (*Account, string, error) { cid := "agt_" + randomHex(8) secret := randomHex(24) hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) if err != nil { return nil, "", err } st := strings.TrimSpace(in.Status) if st == "" { st = StatusActive } if !validStatus(st) { return nil, "", fmt.Errorf("invalid status") } s.seq++ a := &memAcc{ Account: Account{ AgentID: s.seq, TenantID: tenantID, Name: strings.TrimSpace(in.Name), ClientID: cid, HostKey: strings.TrimSpace(in.HostKey), RoleID: in.RoleID, Status: st, Perms: authx.NormalizePerms(uniq(in.Perms)), AppSlugs: uniq(in.AppSlugs), ChannelID: strings.TrimSpace(in.ChannelID), OnlineDBID: strings.TrimSpace(in.OnlineDBID), DatabaseName: strings.TrimSpace(in.DatabaseName), CreatedBy: createdBy, CreatedAt: time.Now().UTC(), }, SecretHash: string(hash), } if a.Name == "" { a.Name = cid } s.byID[a.AgentID] = a s.byCID[a.ClientID] = a cp := cloneAcc(&a.Account) return &cp, secret, nil } func (s *MemoryStore) Update(_ context.Context, tenantID, agentID int64, in UpdateInput) (*Account, error) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok || a.TenantID != tenantID { return nil, fmt.Errorf("agent not found") } if in.Name != nil { a.Name = strings.TrimSpace(*in.Name) } if in.Status != nil { st := strings.TrimSpace(*in.Status) if !validStatus(st) { return nil, fmt.Errorf("invalid status") } a.Status = st } if in.Perms != nil { a.Perms = uniq(*in.Perms) } if in.AppSlugs != nil { a.AppSlugs = uniq(*in.AppSlugs) } if in.RoleID != nil { a.RoleID = *in.RoleID } if in.ChannelID != nil { a.ChannelID = strings.TrimSpace(*in.ChannelID) } if in.OnlineDBID != nil { a.OnlineDBID = strings.TrimSpace(*in.OnlineDBID) } if in.DatabaseName != nil { a.DatabaseName = strings.TrimSpace(*in.DatabaseName) } cp := cloneAcc(&a.Account) return &cp, nil } func (s *MemoryStore) RotateSecret(_ context.Context, tenantID, agentID int64) (string, error) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok || a.TenantID != tenantID { return "", fmt.Errorf("agent not found") } secret := randomHex(24) hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) if err != nil { return "", err } a.SecretHash = string(hash) return secret, nil } func (s *MemoryStore) Delete(_ context.Context, tenantID, agentID int64) error { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok || a.TenantID != tenantID { return fmt.Errorf("agent not found") } delete(s.byCID, a.ClientID) delete(s.byID, agentID) return nil } func (s *MemoryStore) Authenticate(_ context.Context, clientID, clientSecret string) (*Account, error) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byCID[clientID] if !ok { return nil, fmt.Errorf("invalid client credentials") } if bcrypt.CompareHashAndPassword([]byte(a.SecretHash), []byte(clientSecret)) != nil { return nil, fmt.Errorf("invalid client credentials") } switch a.Status { case StatusPending: return nil, fmt.Errorf("agent pending approval") case StatusDisabled: return nil, fmt.Errorf("agent disabled") case StatusActive: // ok default: return nil, fmt.Errorf("agent not active") } cp := cloneAcc(&a.Account) return &cp, nil } func (s *MemoryStore) TouchToken(_ context.Context, agentID int64) error { s.mu.Lock() defer s.mu.Unlock() if a, ok := s.byID[agentID]; ok { now := time.Now().UTC() a.LastTokenAt = &now } return nil } func (s *MemoryStore) HasAppAccess(_ context.Context, agentID int64, slug string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok { return false, fmt.Errorf("agent not found") } // 未配置可访问模块 = 不限制,智能体可自由发布/访问自建模块 if len(a.AppSlugs) == 0 { return true, nil } for _, s0 := range a.AppSlugs { if s0 == "*" || s0 == slug { return true, nil } } return false, nil } func (s *MemoryStore) GrantAppSlug(_ context.Context, agentID int64, slug string) error { slug = strings.TrimSpace(slug) if slug == "" { return fmt.Errorf("empty slug") } s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok { return fmt.Errorf("agent not found") } for _, s0 := range a.AppSlugs { if s0 == slug { return nil } } a.AppSlugs = append(a.AppSlugs, slug) return nil } func (s *MemoryStore) FindByHostKey(_ context.Context, hostKey string) (*Account, error) { hostKey = strings.TrimSpace(hostKey) if hostKey == "" { return nil, fmt.Errorf("host_key required") } s.mu.Lock() defer s.mu.Unlock() for _, a := range s.byID { if a.HostKey == hostKey { cp := cloneAcc(&a.Account) return &cp, nil } } return nil, fmt.Errorf("agent not found") } func (s *MemoryStore) AttachSyncBind(_ context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.byID[agentID] if !ok { return nil, fmt.Errorf("agent not found") } if tenantID > 0 { a.TenantID = tenantID } a.ChannelID = strings.TrimSpace(channelID) a.OnlineDBID = strings.TrimSpace(onlineDBID) a.DatabaseName = strings.TrimSpace(databaseName) if activate { a.Status = StatusActive } cp := cloneAcc(&a.Account) return &cp, nil } type PostgresStore struct { DB *sql.DB } func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} } func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]Account, error) { rows, err := s.DB.QueryContext(ctx, ` SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''), status, created_by, created_at, last_token_at FROM platform_meta.agent_accounts WHERE tenant_id=$1 ORDER BY CASE status WHEN 'pending' THEN 0 WHEN 'active' THEN 1 ELSE 2 END, agent_id`, tenantID) if err != nil { return nil, err } defer rows.Close() var out []Account for rows.Next() { var a Account var last sql.NullTime if err := rows.Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.ChannelID, &a.OnlineDBID, &a.DatabaseName, &a.Status, &a.CreatedBy, &a.CreatedAt, &last); err != nil { return nil, err } if last.Valid { t := last.Time.UTC() a.LastTokenAt = &t } perms, slugs, err := s.loadKids(ctx, a.AgentID) if err != nil { return nil, err } a.Perms, a.AppSlugs = perms, slugs out = append(out, a) } return out, rows.Err() } func (s *PostgresStore) Get(ctx context.Context, tenantID, agentID int64) (*Account, error) { var a Account var last sql.NullTime err := s.DB.QueryRowContext(ctx, ` SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''), status, created_by, created_at, last_token_at FROM platform_meta.agent_accounts WHERE agent_id=$1 AND tenant_id=$2`, agentID, tenantID, ).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.ChannelID, &a.OnlineDBID, &a.DatabaseName, &a.Status, &a.CreatedBy, &a.CreatedAt, &last) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("agent not found") } if err != nil { return nil, err } if last.Valid { t := last.Time.UTC() a.LastTokenAt = &t } perms, slugs, err := s.loadKids(ctx, a.AgentID) if err != nil { return nil, err } a.Perms, a.AppSlugs = perms, slugs return &a, nil } func (s *PostgresStore) Create(ctx context.Context, tenantID, createdBy int64, in CreateInput) (*Account, string, error) { return s.insert(ctx, tenantID, createdBy, in) } func (s *PostgresStore) Register(ctx context.Context, tenantID int64, name, hostKey string) (*Account, string, bool, error) { hostKey = strings.TrimSpace(hostKey) if hostKey != "" { var a Account var last sql.NullTime err := s.DB.QueryRowContext(ctx, ` SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''), status, created_by, created_at, last_token_at FROM platform_meta.agent_accounts WHERE tenant_id=$1 AND host_key=$2`, tenantID, hostKey, ).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.ChannelID, &a.OnlineDBID, &a.DatabaseName, &a.Status, &a.CreatedBy, &a.CreatedAt, &last) if err == nil { if a.Status == StatusPending { secret, err := s.RotateSecret(ctx, tenantID, a.AgentID) if err != nil { return nil, "", false, err } if n := strings.TrimSpace(name); n != "" { _, _ = s.Update(ctx, tenantID, a.AgentID, UpdateInput{Name: &n}) } acc, err := s.Get(ctx, tenantID, a.AgentID) return acc, secret, true, err } return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", a.ClientID, a.Status) } if !errors.Is(err, sql.ErrNoRows) { return nil, "", false, err } } acc, secret, err := s.insert(ctx, tenantID, 0, CreateInput{ Name: name, Status: StatusPending, HostKey: hostKey, }) return acc, secret, false, err } func (s *PostgresStore) insert(ctx context.Context, tenantID, createdBy int64, in CreateInput) (*Account, string, error) { cid := "agt_" + randomHex(8) secret := randomHex(24) hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) if err != nil { return nil, "", err } name := strings.TrimSpace(in.Name) if name == "" { name = cid } st := strings.TrimSpace(in.Status) if st == "" { st = StatusActive } if !validStatus(st) { return nil, "", fmt.Errorf("invalid status") } var a Account err = s.DB.QueryRowContext(ctx, ` INSERT INTO platform_meta.agent_accounts(tenant_id, name, client_id, client_secret_hash, status, created_by, host_key, role_id, channel_id, online_db_id, database_name) VALUES($1,$2,$3,$4,$5,$6,$7,NULLIF($8,0),$9,$10,$11) RETURNING agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''), status, created_by, created_at`, tenantID, name, cid, string(hash), st, createdBy, strings.TrimSpace(in.HostKey), in.RoleID, strings.TrimSpace(in.ChannelID), strings.TrimSpace(in.OnlineDBID), strings.TrimSpace(in.DatabaseName), ).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.ChannelID, &a.OnlineDBID, &a.DatabaseName, &a.Status, &a.CreatedBy, &a.CreatedAt) if err != nil { return nil, "", err } perms, slugs := uniq(in.Perms), uniq(in.AppSlugs) if err := s.replaceKids(ctx, a.AgentID, perms, slugs); err != nil { return nil, "", err } a.Perms, a.AppSlugs = perms, slugs return &a, secret, nil } func (s *PostgresStore) Update(ctx context.Context, tenantID, agentID int64, in UpdateInput) (*Account, error) { a, err := s.Get(ctx, tenantID, agentID) if err != nil { return nil, err } name, status := a.Name, a.Status roleID := a.RoleID channelID, onlineDBID, dbName := a.ChannelID, a.OnlineDBID, a.DatabaseName if in.Name != nil { name = strings.TrimSpace(*in.Name) } if in.Status != nil { status = strings.TrimSpace(*in.Status) if !validStatus(status) { return nil, fmt.Errorf("invalid status") } } if in.RoleID != nil { roleID = *in.RoleID } if in.ChannelID != nil { channelID = strings.TrimSpace(*in.ChannelID) } if in.OnlineDBID != nil { onlineDBID = strings.TrimSpace(*in.OnlineDBID) } if in.DatabaseName != nil { dbName = strings.TrimSpace(*in.DatabaseName) } if _, err := s.DB.ExecContext(ctx, ` UPDATE platform_meta.agent_accounts SET name=$1, status=$2, role_id=NULLIF($3,0), channel_id=$4, online_db_id=$5, database_name=$6 WHERE agent_id=$7 AND tenant_id=$8`, name, status, roleID, channelID, onlineDBID, dbName, agentID, tenantID); err != nil { return nil, err } perms, slugs := a.Perms, a.AppSlugs if in.Perms != nil { perms = uniq(*in.Perms) } if in.AppSlugs != nil { slugs = uniq(*in.AppSlugs) } if err := s.replaceKids(ctx, agentID, perms, slugs); err != nil { return nil, err } return s.Get(ctx, tenantID, agentID) } func (s *PostgresStore) RotateSecret(ctx context.Context, tenantID, agentID int64) (string, error) { if _, err := s.Get(ctx, tenantID, agentID); err != nil { return "", err } secret := randomHex(24) hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) if err != nil { return "", err } _, err = s.DB.ExecContext(ctx, ` UPDATE platform_meta.agent_accounts SET client_secret_hash=$1 WHERE agent_id=$2 AND tenant_id=$3`, string(hash), agentID, tenantID) return secret, err } func (s *PostgresStore) Delete(ctx context.Context, tenantID, agentID int64) error { res, err := s.DB.ExecContext(ctx, ` DELETE FROM platform_meta.agent_accounts WHERE agent_id=$1 AND tenant_id=$2`, agentID, tenantID) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("agent not found") } return nil } func (s *PostgresStore) Authenticate(ctx context.Context, clientID, clientSecret string) (*Account, error) { var a Account var hash string var last sql.NullTime err := s.DB.QueryRowContext(ctx, ` SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''), client_secret_hash, status, created_by, created_at, last_token_at FROM platform_meta.agent_accounts WHERE client_id=$1`, clientID, ).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.ChannelID, &a.OnlineDBID, &a.DatabaseName, &hash, &a.Status, &a.CreatedBy, &a.CreatedAt, &last) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("invalid client credentials") } if err != nil { return nil, err } if bcrypt.CompareHashAndPassword([]byte(hash), []byte(clientSecret)) != nil { return nil, fmt.Errorf("invalid client credentials") } switch a.Status { case StatusPending: return nil, fmt.Errorf("agent pending approval") case StatusDisabled: return nil, fmt.Errorf("agent disabled") case StatusActive: default: return nil, fmt.Errorf("agent not active") } if last.Valid { t := last.Time.UTC() a.LastTokenAt = &t } perms, slugs, err := s.loadKids(ctx, a.AgentID) if err != nil { return nil, err } a.Perms, a.AppSlugs = perms, slugs return &a, nil } func (s *PostgresStore) TouchToken(ctx context.Context, agentID int64) error { _, err := s.DB.ExecContext(ctx, ` UPDATE platform_meta.agent_accounts SET last_token_at=now() WHERE agent_id=$1`, agentID) return err } func (s *PostgresStore) HasAppAccess(ctx context.Context, agentID int64, slug string) (bool, error) { var total int if err := s.DB.QueryRowContext(ctx, ` SELECT COUNT(1) FROM platform_meta.agent_app_grants WHERE agent_id=$1`, agentID).Scan(&total); err != nil { return false, err } // 未配置可访问模块 = 不限制,可自由发布自己的模块 if total == 0 { return true, nil } var n int err := s.DB.QueryRowContext(ctx, ` SELECT COUNT(1) FROM platform_meta.agent_app_grants WHERE agent_id=$1 AND (slug=$2 OR slug='*')`, agentID, slug).Scan(&n) return n > 0, err } func (s *PostgresStore) GrantAppSlug(ctx context.Context, agentID int64, slug string) error { slug = strings.TrimSpace(slug) if slug == "" { return fmt.Errorf("empty slug") } ok, err := s.HasAppAccess(ctx, agentID, slug) if err != nil { return err } if ok { return nil } _, err = s.DB.ExecContext(ctx, ` INSERT INTO platform_meta.agent_app_grants(agent_id, slug) VALUES($1,$2)`, agentID, slug) return err } func (s *PostgresStore) FindByHostKey(ctx context.Context, hostKey string) (*Account, error) { hostKey = strings.TrimSpace(hostKey) if hostKey == "" { return nil, fmt.Errorf("host_key required") } var a Account var last sql.NullTime err := s.DB.QueryRowContext(ctx, ` SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''), status, created_by, created_at, last_token_at FROM platform_meta.agent_accounts WHERE host_key=$1 ORDER BY agent_id DESC LIMIT 1`, hostKey, ).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.ChannelID, &a.OnlineDBID, &a.DatabaseName, &a.Status, &a.CreatedBy, &a.CreatedAt, &last) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("agent not found") } if err != nil { return nil, err } if last.Valid { t := last.Time a.LastTokenAt = &t } perms, slugs, err := s.loadKids(ctx, a.AgentID) if err != nil { return nil, err } a.Perms, a.AppSlugs = perms, slugs return &a, nil } func (s *PostgresStore) AttachSyncBind(ctx context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) { statusSQL := "" args := []any{tenantID, strings.TrimSpace(channelID), strings.TrimSpace(onlineDBID), strings.TrimSpace(databaseName), agentID} if activate { statusSQL = ", status='active'" } res, err := s.DB.ExecContext(ctx, ` UPDATE platform_meta.agent_accounts SET tenant_id=$1, channel_id=$2, online_db_id=$3, database_name=$4`+statusSQL+` WHERE agent_id=$5`, args...) if err != nil { return nil, err } n, _ := res.RowsAffected() if n == 0 { return nil, fmt.Errorf("agent not found") } return s.Get(ctx, tenantID, agentID) } func (s *PostgresStore) loadKids(ctx context.Context, agentID int64) ([]string, []string, error) { prows, err := s.DB.QueryContext(ctx, `SELECT perm FROM platform_meta.agent_permissions WHERE agent_id=$1`, agentID) if err != nil { return nil, nil, err } defer prows.Close() var perms []string for prows.Next() { var p string if err := prows.Scan(&p); err != nil { return nil, nil, err } perms = append(perms, p) } srows, err := s.DB.QueryContext(ctx, `SELECT slug FROM platform_meta.agent_app_grants WHERE agent_id=$1`, agentID) if err != nil { return nil, nil, err } defer srows.Close() var slugs []string for srows.Next() { var slug string if err := srows.Scan(&slug); err != nil { return nil, nil, err } slugs = append(slugs, slug) } return authx.NormalizePerms(perms), slugs, nil } func (s *PostgresStore) replaceKids(ctx context.Context, agentID int64, perms, slugs []string) error { tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return err } defer func() { _ = tx.Rollback() }() if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.agent_permissions WHERE agent_id=$1`, agentID); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.agent_app_grants WHERE agent_id=$1`, agentID); err != nil { return err } for _, p := range authx.NormalizePerms(perms) { if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.agent_permissions(agent_id, perm) VALUES($1,$2)`, agentID, p); err != nil { return err } } for _, slug := range slugs { if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.agent_app_grants(agent_id, slug) VALUES($1,$2)`, agentID, slug); err != nil { return err } } return tx.Commit() } func validStatus(st string) bool { return st == StatusActive || st == StatusPending || st == StatusDisabled } func cloneAcc(a *Account) Account { cp := *a cp.Perms = append([]string{}, a.Perms...) cp.AppSlugs = append([]string{}, a.AppSlugs...) return cp } func uniq(in []string) []string { seen := map[string]struct{}{} out := make([]string, 0, len(in)) for _, s := range in { s = strings.TrimSpace(s) if s == "" { continue } if _, ok := seen[s]; ok { continue } seen[s] = struct{}{} out = append(out, s) } return out } func randomHex(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) }