From 8811d45fd1f1fd54d2e601a469c66991a986c5b4 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 20:56:21 +0000 Subject: [PATCH] feat: conectar sistema centralizado de seguridad al launcher y runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrar admin a security/user-groups.yaml (admins group) - agents.New() ahora acepta acl.ACL pre-resuelta como parámetro; elimina construcción interna desde cfg.Security.Roles - cmd/launcher: carga shellsecurity.Load("security/") al arranque; si falla, WARN + política vacía (open access). Para cada agente llama pksecurity.ResolveACL y pasa la ACL a agents.New() - cmd/launcher/registry.go: stores secPolicy en launchDeps para que reload() también resuelva ACL centralmente - shell/matrix/listener.go: elimina invite gating y allowlist check basados en AllowedUsers; el control de acceso lo hace el runtime - internal/config/schema.go: depreca campos Roles y AllowedUsers (backward compat, no eliminados) - agents/*/config.yaml: elimina bloques security.roles y allowed_users - dev/feature_flags.json: activa centralized-security-groups (enabled: true) Co-Authored-By: Claude Sonnet 4.6 --- agents/asistente-2/config.yaml | 9 -------- agents/assistant-bot/config.yaml | 12 ----------- agents/runtime.go | 14 +++++-------- cmd/launcher/main.go | 35 ++++++++++++++++++++++++++------ cmd/launcher/registry.go | 16 +++++++++------ dev/feature_flags.json | 2 +- internal/config/schema.go | 5 ++++- security/user-groups.yaml | 2 +- shell/matrix/listener.go | 35 -------------------------------- 9 files changed, 50 insertions(+), 80 deletions(-) diff --git a/agents/asistente-2/config.yaml b/agents/asistente-2/config.yaml index 86d8ddc..92192e4 100644 --- a/agents/asistente-2/config.yaml +++ b/agents/asistente-2/config.yaml @@ -162,7 +162,6 @@ matrix: dm_respond: true ignore_bots: true ignore_users: [] - allowed_users: [] # vacío = sin restricción (todos pueden hablar) unauthorized_response: silent # silent | explicit min_power_level: 0 @@ -208,14 +207,6 @@ ssh: # PERMISOS Y SEGURIDAD # ============================================ security: - roles: - admin: - users: ["@admin:matrix-af2f3d.organic-machine.com"] - actions: ["*"] - user: - users: ["*"] - actions: ["*"] - audit: enabled: false log_file: "./agents/asistente-2/data/audit.log" diff --git a/agents/assistant-bot/config.yaml b/agents/assistant-bot/config.yaml index 855013b..aa0a498 100644 --- a/agents/assistant-bot/config.yaml +++ b/agents/assistant-bot/config.yaml @@ -162,10 +162,6 @@ matrix: dm_respond: true # responde en DMs (modo principal por ahora) ignore_bots: true ignore_users: [] - allowed_users: [] # vacío = sin restricción (todos pueden hablar) - # allowed_users: # ejemplo con restricción: - # - "@admin:matrix-af2f3d.organic-machine.com" - # - "@enmanuel:matrix-af2f3d.organic-machine.com" unauthorized_response: silent # silent | explicit min_power_level: 0 # cualquiera puede hablar con el assistant @@ -208,14 +204,6 @@ ssh: # PERMISOS Y SEGURIDAD # ============================================ security: - roles: - admin: - users: ["@admin:matrix-af2f3d.organic-machine.com"] - actions: ["*"] - user: - users: ["*"] - actions: ["*"] - audit: enabled: false log_file: "./agents/assistant-bot/data/audit.log" diff --git a/agents/runtime.go b/agents/runtime.go index 9eacd54..c4a1516 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -115,8 +115,10 @@ func (a *Agent) ClearWindow(roomID string) { } } -// New assembles an Agent from its config, rules, and logger. -func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*Agent, error) { +// New assembles an Agent from its config, rules, pre-resolved ACL, and logger. +// The ACL is resolved externally (e.g. from security/ YAML files) and injected here. +// Pass acl.ACL{} (empty) for open access (no restrictions). +func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logger *slog.Logger) (*Agent, error) { // Matrix client matrixClient, err := matrix.New(cfg.Matrix) if err != nil { @@ -230,14 +232,8 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (* } } - // Build ACL from security roles config - aclRoles := make(map[string]acl.RoleDef, len(cfg.Security.Roles)) - for name, r := range cfg.Security.Roles { - aclRoles[name] = acl.RoleDef{Users: r.Users, Actions: r.Actions} - } - agentACL := acl.FromMap(aclRoles) if !agentACL.Empty() { - logger.Info("acl enabled", "roles", len(cfg.Security.Roles)) + logger.Info("acl enabled (centralized security policy)") } // Tool registry — register tools enabled in config diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 2a85caf..24ce34b 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -26,9 +26,11 @@ import ( "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/pkg/orchestration" + pksecurity "github.com/enmanuel/agents/pkg/security" "github.com/enmanuel/agents/shell/bus" agentlog "github.com/enmanuel/agents/shell/logger" orchshell "github.com/enmanuel/agents/shell/orchestration" + shellsecurity "github.com/enmanuel/agents/shell/security" ) // rulesRegistry maps agent IDs to their rule factories. @@ -81,6 +83,19 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() + // ── Load centralized security policy ── + secPolicy, secErr := shellsecurity.Load("security/") + if secErr != nil { + logger.Warn("security policy load failed, using empty policy (open access)", "err", secErr) + secPolicy = pksecurity.SecurityPolicy{} + } else { + logger.Info("security policy loaded", + "user_groups", len(secPolicy.UserGroups), + "agent_groups", len(secPolicy.AgentGroups), + "policies", len(secPolicy.Policies), + ) + } + // ── Shared bus for inter-agent communication ── agentBus := bus.New(logger) @@ -95,11 +110,12 @@ func main() { // ── Shared dependencies for agent registry ── deps := &launchDeps{ - agentBus: agentBus, - orch: orch, - logDir: logDir, - logLevel: lvl, - parentCtx: ctx, + agentBus: agentBus, + orch: orch, + logDir: logDir, + logLevel: lvl, + parentCtx: ctx, + secPolicy: secPolicy, } registry := newAgentRegistry(deps) @@ -158,7 +174,14 @@ func main() { agentCleanup = func() {} } - a, err := agents.New(cfg, rules, agentLogger) + // Resolve centralized ACL for this agent + agentACL := pksecurity.ResolveACL(cfg.Agent.ID, deps.secPolicy) + agentLogger.Debug("resolved acl for agent", + "agent", cfg.Agent.ID, + "acl_empty", agentACL.Empty(), + ) + + a, err := agents.New(cfg, rules, agentACL, agentLogger) if err != nil { logger.Error("failed to create agent", "id", cfg.Agent.ID, "err", err) agentCleanup() diff --git a/cmd/launcher/registry.go b/cmd/launcher/registry.go index 35b1e83..943b6de 100644 --- a/cmd/launcher/registry.go +++ b/cmd/launcher/registry.go @@ -12,6 +12,7 @@ import ( "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/pkg/orchestration" + pksecurity "github.com/enmanuel/agents/pkg/security" "github.com/enmanuel/agents/shell/bus" agentlog "github.com/enmanuel/agents/shell/logger" ) @@ -27,11 +28,12 @@ type runningAgent struct { // launchDeps holds shared resources needed to start/reload agents. type launchDeps struct { - agentBus *bus.Bus - orch *orchHandle - logDir string - logLevel slog.Level - parentCtx context.Context + agentBus *bus.Bus + orch *orchHandle + logDir string + logLevel slog.Level + parentCtx context.Context + secPolicy pksecurity.SecurityPolicy // centralized security policy loaded from security/ } // agentRegistry tracks all running agents by ID, enabling individual hot-reload. @@ -133,7 +135,9 @@ func (r *agentRegistry) reload(id string, rulesFor func(string, *slog.Logger) [] // 5. Create new agent (validates config before discarding the old one). rules := rulesFor(cfg.Agent.ID, newLogger) - newAgent, err := agents.New(cfg, rules, newLogger) + agentACL := pksecurity.ResolveACL(cfg.Agent.ID, r.deps.secPolicy) + newLogger.Debug("resolved acl for agent (reload)", "agent", cfg.Agent.ID, "acl_empty", agentACL.Empty()) + newAgent, err := agents.New(cfg, rules, agentACL, newLogger) if err != nil { newLogger.Error("reload: failed to create agent", "id", id, "err", err) newCleanup() diff --git a/dev/feature_flags.json b/dev/feature_flags.json index 3a43dad..3f80704 100644 --- a/dev/feature_flags.json +++ b/dev/feature_flags.json @@ -7,7 +7,7 @@ "added": "2026-03-07" }, "centralized-security-groups": { - "enabled": false, + "enabled": true, "issue": "0024", "description": "Sistema centralizado de grupos de usuarios y agentes para control de acceso; elimina security.roles y allowed_users por agente", "added": "2026-03-08" diff --git a/internal/config/schema.go b/internal/config/schema.go index 41df2e5..de7c89f 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -227,7 +227,8 @@ type FiltersCfg struct { DMRespond bool `yaml:"dm_respond"` IgnoreBots bool `yaml:"ignore_bots"` IgnoreUsers []string `yaml:"ignore_users"` - AllowedUsers []string `yaml:"allowed_users"` // allowlist (empty = allow all) + // Deprecated: use security/ centralized groups instead. Kept for backward compatibility. + AllowedUsers []string `yaml:"allowed_users"` UnauthorizedResponse string `yaml:"unauthorized_response"` // silent (default) | explicit MinPowerLevel int `yaml:"min_power_level"` } @@ -287,6 +288,8 @@ type SSHTargetCfg struct { // ── Security ────────────────────────────────────────────────────────────── type SecurityCfg struct { + // Deprecated: use security/ centralized groups instead (see security/user-groups.yaml, permissions.yaml). + // Kept for backward compatibility; will be removed in a future issue. Roles map[string]RoleCfg `yaml:"roles"` Audit AuditCfg `yaml:"audit"` Secrets SecretsCfg `yaml:"secrets"` diff --git a/security/user-groups.yaml b/security/user-groups.yaml index 1a8ff46..ea3e315 100644 --- a/security/user-groups.yaml +++ b/security/user-groups.yaml @@ -2,6 +2,6 @@ # Members: lista de Matrix user IDs, o "*" para todos los usuarios groups: admins: - members: [] # rellenar con los administradores reales + members: ["@admin:matrix-af2f3d.organic-machine.com"] everyone: members: ["*"] diff --git a/shell/matrix/listener.go b/shell/matrix/listener.go index 3f9657f..c1b7f96 100644 --- a/shell/matrix/listener.go +++ b/shell/matrix/listener.go @@ -102,22 +102,6 @@ func (l *Listener) Run(ctx context.Context) error { return } - // Invite gating: if allowlist is configured, reject invites from unauthorized users - if len(l.cfg.Filters.AllowedUsers) > 0 { - allowed := false - for _, u := range l.cfg.Filters.AllowedUsers { - if evt.Sender.String() == u { - allowed = true - break - } - } - if !allowed { - l.logger.Info("rejecting invite from unauthorized user", - "room", evt.RoomID, "inviter", evt.Sender) - return - } - } - l.logger.Info("received room invite, joining", "room", evt.RoomID, "inviter", evt.Sender) if _, err := l.client.raw.JoinRoom(ctx, evt.RoomID.String(), "", nil); err != nil { l.logger.Error("failed to auto-join room", "room", evt.RoomID, "err", err) @@ -281,25 +265,6 @@ func (l *Listener) shouldHandle(evt *event.Event) bool { } } - // Check allowlist — if configured, only allowed users can talk to the bot - if len(f.AllowedUsers) > 0 { - allowed := false - for _, u := range f.AllowedUsers { - if evt.Sender.String() == u { - allowed = true - break - } - } - if !allowed { - l.logger.Debug("ignoring unauthorized user", "sender", evt.Sender) - if f.UnauthorizedResponse == "explicit" { - ctx := context.Background() - _ = l.client.SendText(ctx, evt.RoomID.String(), "No tienes permisos para interactuar con este agente.") - } - return false - } - } - // Check if room is in the listen list if len(l.cfg.Rooms.Listen) > 0 { allowed := false