feat(channels): add channel context scope contracts

This commit is contained in:
Goon committed 2026-05-29 19:11:08 +07:00
1 parent 07bdec3c08
commit d230228217
6 files changed
+155 -7

No files matched your search

+136
View File
@@ -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)
}
+7 -6
View File
@@ -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).
+2
View File
@@ -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"
)
+5 -1
View File
@@ -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)
+1
View File
@@ -40,6 +40,7 @@ type RunContext struct {
// Tool configuration
BuiltinToolSettings map[string][]byte
ChannelType string
ChannelContextScope ChannelContextScope
SubagentsCfg *config.SubagentsConfig
ParentModel string
ParentProvider string
+4
View File
@@ -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)