# Task 10 — Control de acceso a agentes por usuario

## Objetivo

Implementar un sistema de control de acceso que permita restringir qué usuarios pueden interactuar con cada agente, usando la infraestructura `SecurityCfg` ya existente (declarada pero no enforceada).

## Contexto actual

- `SecurityCfg` ya existe en `internal/config/schema.go` con `Roles` (map de rol -> users + actions)
- `FiltersCfg` ya tiene `ignore_users` (blocklist) y `min_power_level`
- `shouldHandle()` en `shell/matrix/listener.go` filtra por room y blocklist, pero NO por allowlist
- Auto-join de invites es incondicional (acepta cualquier invite)
- `MatchMinPowerLevel()` en `pkg/decision/engine.go` existe pero `powerLevel` siempre se pasa como 0
- Los configs de agentes ya definen roles pero nadie los verifica:
  ```yaml
  security:
    roles:
      admin:
        users: ["@admin:matrix-af2f3d.organic-machine.com"]
        actions: ["*"]
      user:
        users: ["*"]
        actions: ["ask", "help", "summarize"]
  ```

## Problema

- Cualquier usuario del homeserver (o federado) puede invitar a un bot y hablar con el
- No hay forma de restringir acceso por usuario — los bots son publicos
- Los roles configurados en `security.roles` no se verifican en ningun punto
- El auto-join acepta invites de cualquiera sin verificar permisos
- No hay distincion entre acciones permitidas por rol (admin vs user)

## Diseno

### Arquitectura (pure core / impure shell)

```
pkg/acl/             -> PURE: tipos AccessList, CheckAccess(), ExtractRole()
shell/matrix/listener.go -> IMPURE: aplica ACL en shouldHandle() y auto-join
agents/runtime.go    -> composicion: pasa ACL al listener, verifica roles en comandos
```

### Modelo de acceso

Tres niveles de control, cada uno incrementa la restriccion:

1. **Nivel 1 — Allowlist de usuarios** (quien puede hablar con el bot)
2. **Nivel 2 — Invite gating** (quien puede invitar al bot a una sala)
3. **Nivel 3 — RBAC por accion** (quien puede ejecutar que comandos/acciones)

### Nivel 1 — Allowlist en FiltersCfg

Agregar `allowed_users` a `FiltersCfg`:

```go
// internal/config/schema.go
type FiltersCfg struct {
    // ... existente ...
    AllowedUsers []string `yaml:"allowed_users"` // allowlist (vacio = todos)
}
```

Config YAML:
```yaml
matrix:
  filters:
    allowed_users:
      - "@admin:matrix-af2f3d.organic-machine.com"
      - "@enmanuel:matrix-af2f3d.organic-machine.com"
    # vacio o ausente = sin restriccion (todos pueden hablar)
```

Verificacion en `shouldHandle()`:
```go
// Despues de los filtros existentes, antes de return true
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)
        return false
    }
}
```

### Nivel 2 — Invite gating

Modificar el handler de `StateMember` invite para verificar quien invita:

```go
// shell/matrix/listener.go — en el handler de invites
if membership != event.MembershipInvite {
    return
}

// Verificar si el invitante esta autorizado
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)
        // Opcion: leave room o simplemente no joinear
        return
    }
}

// Auto-join (existente)
l.client.raw.JoinRoom(ctx, evt.RoomID.String(), "", nil)
```

### Nivel 3 — RBAC por accion (conectar SecurityCfg.Roles)

#### Nuevo paquete `pkg/acl/` (puro)

```go
// pkg/acl/types.go

// Role representa un rol con sus usuarios y acciones permitidas.
type Role struct {
    Name    string
    Users   []string // Matrix user IDs, "*" = todos
    Actions []string // acciones permitidas, "*" = todas
}

// ACL contiene la lista de control de acceso resuelta.
type ACL struct {
    Roles []Role
}
```

```go
// pkg/acl/check.go

// FromConfig construye un ACL desde el mapa de roles del config. Puro.
func FromConfig(roles map[string]config.RoleCfg) ACL { ... }

// RoleFor devuelve el nombre del rol con mayor privilegio para un userID. Puro.
// Prioridad: el primer rol especifico que matchee; si ninguno, busca "*".
// Si no hay match, retorna "" (sin rol = sin acceso si RBAC esta activo).
func (a ACL) RoleFor(userID string) string { ... }

// CanDo verifica si un userID puede ejecutar una accion. Puro.
// Si no hay roles definidos, retorna true (sin RBAC = acceso libre).
// Si hay roles pero el usuario no tiene ninguno, retorna false.
func (a ACL) CanDo(userID string, action string) bool { ... }

// AllowedUsers retorna la lista consolidada de todos los userIDs
// con al menos un rol (excluyendo "*"). Util para allowlist. Puro.
func (a ACL) AllowedUsers() []string { ... }
```

#### Integracion en runtime.go

```go
// agents/runtime.go — en handleEvent, despues de evaluar el comando

// Para comandos built-in, verificar accion "command:<name>"
if handler, ok := a.commands[msgCtx.Command]; ok {
    if !a.acl.CanDo(msgCtx.SenderID, "command:"+msgCtx.Command) {
        a.matrix.SendText(ctx, roomID, "No tienes permisos para este comando.")
        return
    }
    reply := handler(ctx, msgCtx)
    // ...
}

// Para tool commands, verificar accion "tool:<name>"
// Para LLM fallback, verificar accion "ask" (o la que corresponda)
```

#### Mapeo de acciones

| Accion config  | Que protege                                  |
|----------------|----------------------------------------------|
| `*`            | Todo (wildcard)                              |
| `ask`          | Hablar con el LLM (mensajes de texto libre)  |
| `command:*`    | Todos los comandos !xxx                      |
| `command:tool` | Ejecutar !tool                               |
| `command:clear`| Ejecutar !clear                              |
| `tool:*`       | Todas las tools via LLM                      |
| `tool:ssh_command` | Tool SSH especifica                      |
| `help`         | Comandos informativos (!help, !info, !status)|

### Retrocompatibilidad

- Si `allowed_users` esta vacio → sin restriccion (como ahora)
- Si `security.roles` esta vacio → sin RBAC (como ahora)
- El comportamiento por defecto NO cambia — todo sigue abierto a menos que se configure

### Respuesta a usuarios no autorizados

Dos estrategias configurables:

1. **Silent** (default): ignorar mensajes de usuarios no autorizados (como si el bot no existiera)
2. **Explicit**: responder con "No tienes permisos para interactuar con este agente"

```yaml
matrix:
  filters:
    allowed_users: [...]
    unauthorized_response: "silent"  # silent | explicit
```

## Tareas de implementacion

### Fase 1 — Allowlist basica (Nivel 1)
- [ ] Agregar `AllowedUsers []string` a `FiltersCfg` en `internal/config/schema.go`
- [ ] Agregar `UnauthorizedResponse string` a `FiltersCfg` (`silent` | `explicit`)
- [ ] Implementar check de allowlist en `shouldHandle()` de `shell/matrix/listener.go`
- [ ] Si `unauthorized_response: explicit`, responder antes de retornar false
- [ ] Tests: shouldHandle con allowlist vacia (pasa todo), con lista (filtra)

### Fase 2 — Invite gating (Nivel 2)
- [ ] Modificar handler de `StateMember` invite en listener.go
- [ ] Verificar invitante contra `allowed_users` antes de auto-join
- [ ] Si no autorizado: no joinear (y opcionalmente leave/reject)
- [ ] Log de invites rechazados

### Fase 3 — RBAC puro (Nivel 3)
- [ ] Crear `pkg/acl/types.go` — tipos Role, ACL
- [ ] Crear `pkg/acl/check.go` — FromConfig, RoleFor, CanDo, AllowedUsers
- [ ] Crear `pkg/acl/check_test.go` — tests exhaustivos del ACL puro
- [ ] Tests: wildcard "*" en users, wildcard "*" en actions, sin roles, multiples roles

### Fase 4 — Conectar RBAC al runtime
- [ ] Construir ACL en `agents/runtime.go` New() desde `cfg.Security.Roles`
- [ ] Verificar permisos antes de ejecutar comandos built-in
- [ ] Verificar permisos antes de ejecutar tools (via LLM y via !tool)
- [ ] Verificar permiso "ask" antes de enviar al LLM
- [ ] Respuesta de "sin permisos" respetuosa cuando se deniega

### Fase 5 — Config y documentacion
- [ ] Actualizar configs de assistant-bot y asistente-2 con ejemplo de allowed_users
- [ ] Documentar en `docs/creating-agents.md` la seccion de control de acceso
- [ ] Verificar que agentes sin security config siguen funcionando (retrocompat)

### Fase 6 (futura) — Extensiones
- [ ] Audit log: registrar intentos de acceso denegados en audit log
- [ ] Patron glob en users: `@*:matrix-af2f3d.organic-machine.com` (solo usuarios locales)
- [ ] Rate limiting por rol (admin sin limite, user con rate limit)
- [ ] Comando `!acl` para admins: ver roles activos, verificar permisos de un usuario
