diff --git a/internal/store/channel_context_store.go b/internal/store/channel_context_store.go new file mode 100644 index 00000000..c6775e9e --- /dev/null +++ b/internal/store/channel_context_store.go @@ -0,0 +1,136 @@ +package store + +import ( + "context" + "encoding/json" + "time" + + "github.com/google/uuid" +) + +const ( + ChannelScopeTypeChannel = "channel" + ChannelScopeTypeGroup = "group" + ChannelScopeTypeUser = "user" + ChannelScopeTypeRole = "role" +) + +// ChannelContextScope identifies the channel-level context used for scoped +// grants and credentials. ChannelInstanceName lets runtime code resolve scope +// without a channel_instances lookup at the call site. +type ChannelContextScope struct { + ChannelInstanceID uuid.UUID `json:"channel_instance_id,omitempty"` + ChannelInstanceName string `json:"channel_instance_name,omitempty"` + ScopeType string `json:"scope_type"` + ScopeKey string `json:"scope_key"` +} + +func (s ChannelContextScope) Valid() bool { + if s.ScopeType == "" { + return false + } + if s.ChannelInstanceID == uuid.Nil && s.ChannelInstanceName == "" { + return false + } + return s.ScopeType == ChannelScopeTypeChannel || s.ScopeKey != "" +} + +func WithChannelContextScope(ctx context.Context, scope ChannelContextScope) context.Context { + return context.WithValue(ctx, ChannelContextScopeKey, scope) +} + +func ChannelContextScopeFromContext(ctx context.Context) (ChannelContextScope, bool) { + if v, ok := ctx.Value(ChannelContextScopeKey).(ChannelContextScope); ok && v.Valid() { + return v, true + } + if rc := RunContextFromCtx(ctx); rc != nil && rc.ChannelContextScope.Valid() { + return rc.ChannelContextScope, true + } + return ChannelContextScope{}, false +} + +type MCPContextGrant struct { + ID uuid.UUID `json:"id" db:"id"` + ChannelInstanceID uuid.UUID `json:"channel_instance_id" db:"channel_instance_id"` + ScopeType string `json:"scope_type" db:"scope_type"` + ScopeKey string `json:"scope_key" db:"scope_key"` + ServerID uuid.UUID `json:"server_id" db:"server_id"` + Enabled bool `json:"enabled" db:"enabled"` + ToolAllow json.RawMessage `json:"tool_allow,omitempty" db:"tool_allow"` + ToolDeny json.RawMessage `json:"tool_deny,omitempty" db:"tool_deny"` + ConfigOverrides json.RawMessage `json:"config_overrides,omitempty" db:"config_overrides"` + GrantedBy string `json:"granted_by" db:"granted_by"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type MCPContextCredentials struct { + ID uuid.UUID `json:"id" db:"id"` + ChannelInstanceID uuid.UUID `json:"channel_instance_id" db:"channel_instance_id"` + ScopeType string `json:"scope_type" db:"scope_type"` + ScopeKey string `json:"scope_key" db:"scope_key"` + ServerID uuid.UUID `json:"server_id" db:"server_id"` + APIKey string `json:"api_key,omitempty" db:"-"` + Headers map[string]string `json:"headers,omitempty" db:"-"` + Env map[string]string `json:"env,omitempty" db:"-"` + CreatedBy string `json:"created_by" db:"created_by"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type SecureCLIContextGrant struct { + ID uuid.UUID `json:"id" db:"id"` + ChannelInstanceID uuid.UUID `json:"channel_instance_id" db:"channel_instance_id"` + ScopeType string `json:"scope_type" db:"scope_type"` + ScopeKey string `json:"scope_key" db:"scope_key"` + BinaryID uuid.UUID `json:"binary_id" db:"binary_id"` + Enabled bool `json:"enabled" db:"enabled"` + DenyArgs *json.RawMessage `json:"deny_args,omitempty" db:"deny_args"` + DenyVerbose *json.RawMessage `json:"deny_verbose,omitempty" db:"deny_verbose"` + TimeoutSeconds *int `json:"timeout_seconds,omitempty" db:"timeout_seconds"` + Tips *string `json:"tips,omitempty" db:"tips"` + EncryptedEnv []byte `json:"-" db:"encrypted_env"` + EnvKeys []string `json:"env_keys,omitempty" db:"-"` + Env map[string]SecureCLIEnvResponseEntry `json:"env,omitempty" db:"-"` + EnvSet bool `json:"env_set" db:"-"` + GrantedBy string `json:"granted_by" db:"granted_by"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type SecureCLIContextCredentials struct { + ID uuid.UUID `json:"id" db:"id"` + ChannelInstanceID uuid.UUID `json:"channel_instance_id" db:"channel_instance_id"` + ScopeType string `json:"scope_type" db:"scope_type"` + ScopeKey string `json:"scope_key" db:"scope_key"` + BinaryID uuid.UUID `json:"binary_id" db:"binary_id"` + EncryptedEnv []byte `json:"-" db:"encrypted_env"` + Metadata json.RawMessage `json:"metadata,omitempty" db:"metadata"` + CredentialType *string `json:"credential_type,omitempty" db:"credential_type"` + HostScope *string `json:"host_scope,omitempty" db:"host_scope"` + CreatedBy string `json:"created_by" db:"created_by"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type MCPContextAdminStore interface { + UpsertContextGrant(ctx context.Context, grant *MCPContextGrant) error + DeleteContextGrant(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, serverID uuid.UUID) error + ListContextGrants(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]MCPContextGrant, error) + + SetContextCredentials(ctx context.Context, creds *MCPContextCredentials) error + GetContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, serverID uuid.UUID) (*MCPContextCredentials, error) + DeleteContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, serverID uuid.UUID) error + ListContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]MCPContextCredentials, error) +} + +type SecureCLIContextAdminStore interface { + UpsertContextGrant(ctx context.Context, grant *SecureCLIContextGrant) error + DeleteContextGrant(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, binaryID uuid.UUID) error + ListContextGrants(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]SecureCLIContextGrant, error) + + SetContextCredentials(ctx context.Context, creds *SecureCLIContextCredentials) error + GetContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, binaryID uuid.UUID) (*SecureCLIContextCredentials, error) + DeleteContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, binaryID uuid.UUID) error + ListContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]SecureCLIContextCredentials, error) +} diff --git a/internal/store/contact_store.go b/internal/store/contact_store.go index 62b6ff5b..584d3d13 100644 --- a/internal/store/contact_store.go +++ b/internal/store/contact_store.go @@ -29,12 +29,13 @@ type ChannelContact struct { // ContactListOpts holds pagination and filter options for listing contacts. type ContactListOpts struct { - Search string // ILIKE on display_name, username, sender_id - ChannelType string // filter by platform (telegram, discord, etc.) - PeerKind string // "direct" or "group" - ContactType string // "user" or "group" - Limit int - Offset int + Search string // ILIKE on display_name, username, sender_id + ChannelType string // filter by platform (telegram, discord, etc.) + ChannelInstance string // filter by channel instance name + PeerKind string // "direct" or "group" + ContactType string // "user" or "group" + Limit int + Offset int } // ContactStore manages channel contacts (auto-collected user info). diff --git a/internal/store/context.go b/internal/store/context.go index 4b0f3a78..50111216 100644 --- a/internal/store/context.go +++ b/internal/store/context.go @@ -50,6 +50,8 @@ const ( CredentialUserIDKey contextKey = "goclaw_credential_user_id" // SenderNameKey is the display name from channel metadata (for bootstrap auto-contact). SenderNameKey contextKey = "goclaw_sender_name" + // ChannelContextScopeKey carries the channel/group/user scope for runtime grants and credentials. + ChannelContextScopeKey contextKey = "goclaw_channel_context_scope" // AgentAudioKey carries the immutable agent audio snapshot for TTS tool dispatch. AgentAudioKey contextKey = "goclaw_agent_audio" ) diff --git a/internal/store/pg/channel_contacts.go b/internal/store/pg/channel_contacts.go index 2563b090..92788091 100644 --- a/internal/store/pg/channel_contacts.go +++ b/internal/store/pg/channel_contacts.go @@ -11,7 +11,6 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/store" ) - // PGContactStore implements store.ContactStore backed by Postgres. type PGContactStore struct { db *sql.DB @@ -64,6 +63,11 @@ func contactWhereClause(ctx context.Context, opts store.ContactListOpts) (string args = append(args, opts.ChannelType) argIdx++ } + if opts.ChannelInstance != "" { + conditions = append(conditions, fmt.Sprintf("channel_instance = $%d", argIdx)) + args = append(args, opts.ChannelInstance) + argIdx++ + } if opts.PeerKind != "" { conditions = append(conditions, fmt.Sprintf("peer_kind = $%d", argIdx)) args = append(args, opts.PeerKind) diff --git a/internal/store/run_context.go b/internal/store/run_context.go index 2e05a356..2c825571 100644 --- a/internal/store/run_context.go +++ b/internal/store/run_context.go @@ -40,6 +40,7 @@ type RunContext struct { // Tool configuration BuiltinToolSettings map[string][]byte ChannelType string + ChannelContextScope ChannelContextScope SubagentsCfg *config.SubagentsConfig ParentModel string ParentProvider string diff --git a/internal/store/sqlitestore/contacts.go b/internal/store/sqlitestore/contacts.go index a48aabe7..f0db1857 100644 --- a/internal/store/sqlitestore/contacts.go +++ b/internal/store/sqlitestore/contacts.go @@ -71,6 +71,10 @@ func contactWhereSQLite(ctx context.Context, opts store.ContactListOpts) (string conditions = append(conditions, "channel_type = ?") args = append(args, opts.ChannelType) } + if opts.ChannelInstance != "" { + conditions = append(conditions, "channel_instance = ?") + args = append(args, opts.ChannelInstance) + } if opts.PeerKind != "" { conditions = append(conditions, "peer_kind = ?") args = append(args, opts.PeerKind)