From 8811d45fd1f1fd54d2e601a469c66991a986c5b4 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 20:56:21 +0000 Subject: [PATCH 1/2] 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 From c5748212f85776a9eaaa6efb32f2f55a02e91e89 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 20:57:28 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20cerrar=20issue=200024c=20=E2=80=94?= =?UTF-8?q?=20security=20integration=20+=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Actualiza docs/security.md: nueva sección "Sistema de grupos centralizados" con estructura de los 3 YAML, acciones disponibles, campos deprecados - Actualiza .claude/CLAUDE.md: añade security/ en la estructura del proyecto - Mueve 0024 y 0024c a dev/issues/completed/ - Actualiza dev/issues/README.md: marca 0024, 0024a, 0024b, 0024c como completado Co-Authored-By: Claude Sonnet 4.6 --- .claude/CLAUDE.md | 1 + dev/issues/README.md | 4 +- .../0024-centralized-security-groups.md | 0 .../0024c-security-integration.md | 0 docs/security.md | 79 +++++++++++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) rename dev/issues/{ => completed}/0024-centralized-security-groups.md (100%) rename dev/issues/{ => completed}/0024c-security-integration.md (100%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d43ed59..3401623 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -57,6 +57,7 @@ agents/runtime.go Agent{}: ensambla core + shell agents// agent.go (reglas puras) + config.yaml + prompts/system.md tools/ tool registry + tool implementations (subpackages) internal/config/ schema.go + loader.go +security/ grupos de usuarios/agentes + politicas de permisos (YAMLs) cmd/launcher/ entrypoint principal (rulesRegistry) cmd/agentctl/ CLI de gestion crons/ catálogo de automatizaciones nombradas (schedule.yaml + prompts) diff --git a/dev/issues/README.md b/dev/issues/README.md index 5182250..57648cf 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -31,8 +31,8 @@ afectados y notas de implementacion. | 22b | E2E: Auth fixtures y helpers | [0022b-e2e-auth-helpers.md](completed/0022b-e2e-auth-helpers.md) | completado | | 22c | E2E: Tests de agentes + docs | [0022c-e2e-agent-tests.md](completed/0022c-e2e-agent-tests.md) | completado | | 23 | Seccion de tests en dashboard | [0023-dashboard-tests.md](completed/0023-dashboard-tests.md) | completado | -| 24 | Grupos y permisos centralizados | [0024-centralized-security-groups.md](0024-centralized-security-groups.md) | pendiente | +| 24 | Grupos y permisos centralizados | [0024-centralized-security-groups.md](completed/0024-centralized-security-groups.md) | completado | | 24a | Security types: pkg/security/ | [0024a-security-types.md](completed/0024a-security-types.md) | completado | | 24b | Security loader: shell/security/ | [0024b-security-loader.md](completed/0024b-security-loader.md) | completado | -| 24c | Security integration + cleanup | [0024c-security-integration.md](0024c-security-integration.md) | pendiente | +| 24c | Security integration + cleanup | [0024c-security-integration.md](completed/0024c-security-integration.md) | completado | | 25 | Catálogo cron + scaffolder | [0025-cron-scaffolder.md](completed/0025-cron-scaffolder.md) | completado | diff --git a/dev/issues/0024-centralized-security-groups.md b/dev/issues/completed/0024-centralized-security-groups.md similarity index 100% rename from dev/issues/0024-centralized-security-groups.md rename to dev/issues/completed/0024-centralized-security-groups.md diff --git a/dev/issues/0024c-security-integration.md b/dev/issues/completed/0024c-security-integration.md similarity index 100% rename from dev/issues/0024c-security-integration.md rename to dev/issues/completed/0024c-security-integration.md diff --git a/docs/security.md b/docs/security.md index ae3d26f..029e1de 100644 --- a/docs/security.md +++ b/docs/security.md @@ -137,6 +137,85 @@ Esto evita que el subproceso `claude -p` tenga acceso de lectura/escritura al co Implementado en `shell/llm/claudecode.go` → `resolveWorkDir()`. +## 7. Sistema de grupos centralizados (`security/`) + +Control de acceso centralizado: quien puede hablar con que agentes, y que puede hacer. +Reemplaza los campos per-agente `security.roles` y `matrix.filters.allowed_users` (ahora deprecados). + +### Estructura de archivos + +``` +security/ + user-groups.yaml # grupos de usuarios Matrix + agent-groups.yaml # grupos de agentes + permissions.yaml # politicas: que grupo de usuarios tiene que acciones en que agentes +``` + +### user-groups.yaml + +```yaml +groups: + admins: + members: ["@admin:matrix-af2f3d.organic-machine.com"] + developers: + members: ["@dev1:homeserver.com", "@dev2:homeserver.com"] + everyone: + members: ["*"] # wildcard: todos los usuarios +``` + +### agent-groups.yaml + +```yaml +groups: + all: + agents: ["*"] # wildcard: todos los agentes + production: + agents: ["assistant-bot", "asistente-2"] +``` + +### permissions.yaml + +```yaml +policies: + - agent_group: all # aplica a todos los agentes + permissions: + - user_group: admins + actions: ["*"] # admins pueden hacer todo + - user_group: everyone + actions: ["ask"] # todos pueden chatear + - agent_group: production # solo agentes de produccion + permissions: + - user_group: developers + actions: ["ask", "command:deploy", "tool:ssh_command"] +``` + +### Acciones disponibles + +| Accion | Descripcion | +|--------|-------------| +| `*` | Todo permitido | +| `ask` | Puede chatear con el agente (mensajes normales) | +| `command:` | Puede ejecutar el comando `!` | +| `tool:` | El LLM puede llamar la tool `` para este usuario | + +### Resolucion de ACL + +Al arrancar, el launcher: +1. Carga todos los YAMLs con `shellsecurity.Load("security/")` +2. Para cada agente, llama `pksecurity.ResolveACL(agentID, policy)` +3. Inyecta la `acl.ACL` resultante en `agents.New()` + +**Comportamiento cuando la politica esta vacia**: si `security/` no existe o no hay politicas que apliquen al agente, la ACL esta vacia y el acceso es abierto (sin restricciones). Preferible a denegar todo por defecto en produccion. + +### Campos deprecados + +Los siguientes campos en `config.yaml` del agente estan deprecated y no tienen efecto desde la activacion del sistema centralizado: + +- `security.roles` — reemplazado por `security/permissions.yaml` +- `matrix.filters.allowed_users` — reemplazado por `security/user-groups.yaml` + +Los campos siguen en el schema para compatibilidad con configs existentes y se eliminarán en un issue futuro. + ## Activacion Para activar todas las protecciones, añadir al `config.yaml` del agente: