Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
104 lines
4.2 KiB
Go
104 lines
4.2 KiB
Go
package im
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type verifiedAgentExecutionKey struct{}
|
|
type verifiedAgentExecution struct {
|
|
TenantID uint64
|
|
AgentID, OwnerID, ChannelID string
|
|
PrincipalID string
|
|
}
|
|
|
|
// IsVerifiedAgentExecution accepts only a proof minted inside the IM runtime
|
|
// after reading an enabled channel and its exact Agent from durable storage.
|
|
// A synthetic user, IM principal or request parameter alone never grants it.
|
|
func IsVerifiedAgentExecution(ctx context.Context, agent *types.CustomAgent) bool {
|
|
proof, ok := ctx.Value(verifiedAgentExecutionKey{}).(verifiedAgentExecution)
|
|
if !ok || agent == nil || proof.ChannelID == "" || proof.AgentID != agent.ID || proof.TenantID != agent.TenantID || proof.OwnerID != agent.CreatedBy {
|
|
return false
|
|
}
|
|
tenant, hasTenant := types.TenantIDFromContext(ctx)
|
|
user, hasUser := types.UserIDFromContext(ctx)
|
|
principal, hasPrincipal := types.PrincipalFromContext(ctx)
|
|
return hasTenant && hasUser && tenant == proof.TenantID && user == fmt.Sprintf("system-%d", tenant) &&
|
|
hasPrincipal && principal.Type == types.PrincipalIMUser && principal.ID == proof.PrincipalID
|
|
}
|
|
|
|
func (s *Service) resolveConfiguredChannelExecution(ctx context.Context, channel *IMChannel) (context.Context, *types.CustomAgent, error) {
|
|
agent, err := s.resolveConfiguredChannelAgent(ctx, channel)
|
|
if err != nil {
|
|
return ctx, nil, err
|
|
}
|
|
principal, hasPrincipal := types.PrincipalFromContext(ctx)
|
|
tenant, hasTenant := types.TenantIDFromContext(ctx)
|
|
user, hasUser := types.UserIDFromContext(ctx)
|
|
if !hasPrincipal || principal.Type != types.PrincipalIMUser || !hasTenant || tenant != channel.TenantID || !hasUser || user != fmt.Sprintf("system-%d", tenant) {
|
|
return ctx, nil, fmt.Errorf("verified IM identity is required for channel execution")
|
|
}
|
|
proof := verifiedAgentExecution{TenantID: channel.TenantID, AgentID: agent.ID, OwnerID: agent.CreatedBy, ChannelID: channel.ID, PrincipalID: principal.ID}
|
|
return context.WithValue(ctx, verifiedAgentExecutionKey{}, proof), agent, nil
|
|
}
|
|
|
|
// InvalidateDeletedChannels is called only after durable deletion commits.
|
|
func (s *Service) InvalidateDeletedChannels(ids []string) {
|
|
for _, id := range ids {
|
|
s.StopChannel(id)
|
|
s.publishChannelConfigChange(id)
|
|
}
|
|
}
|
|
|
|
func (s *Service) cancelChannelRequests(id string) {
|
|
s.inflight.Range(func(key, value any) bool {
|
|
userKey, ok := key.(string)
|
|
if ok && strings.HasPrefix(userKey, id+":") {
|
|
if entry, ok := value.(*inflightEntry); ok && entry.cancel != nil {
|
|
entry.cancel()
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// Serialize authoring with agent deletion so a concurrent channel create or
|
|
// reassignment cannot leave a new channel pointing at a deleted custom agent.
|
|
func lockChannelAgent(tx *gorm.DB, channel *IMChannel) error {
|
|
if types.IsBuiltinAgentID(channel.AgentID) {
|
|
return nil // Built-in agents cannot be deleted.
|
|
}
|
|
var agent types.CustomAgent
|
|
return tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").
|
|
Where("id = ? AND tenant_id = ?", channel.AgentID, channel.TenantID).First(&agent).Error
|
|
}
|
|
|
|
// resolveConfiguredChannelAgent is a trusted runtime path, not a user Agent
|
|
// lookup. A persisted, enabled channel is the explicit authorization to execute
|
|
// exactly its configured Agent, including an owner's private digital human.
|
|
func (s *Service) resolveConfiguredChannelAgent(ctx context.Context, channel *IMChannel) (*types.CustomAgent, error) {
|
|
if channel == nil || channel.ID == "" || channel.AgentID == "" {
|
|
return nil, fmt.Errorf("channel has no configured agent")
|
|
}
|
|
var fresh IMChannel
|
|
if err := s.db.WithContext(ctx).Where("id = ? AND tenant_id = ? AND agent_id = ? AND enabled = ?", channel.ID, channel.TenantID, channel.AgentID, true).First(&fresh).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
var agent types.CustomAgent
|
|
err := s.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", fresh.AgentID, fresh.TenantID).First(&agent).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) && types.IsBuiltinAgentID(fresh.AgentID) {
|
|
return s.agentService.GetAgentByID(ctx, fresh.AgentID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
agent.EnsureDefaults()
|
|
return &agent, nil
|
|
}
|