# 0024a — Security types: pkg/security/ — tipos puros y resolución ACL

> Parte a del issue [0024-centralized-security-groups.md](0024-centralized-security-groups.md).

## Objetivo

Crear el paquete puro `pkg/security/` con los tipos `UserGroup`, `AgentGroup`, `SecurityPolicy` y la función `ResolveACL(agentID, policy) → acl.ACL`. Este paquete es el núcleo de resolución del sistema centralizado de permisos.

## Contexto

- `pkg/acl/` ya existe con `ACL`, `Role`, `CanDo()`, `RoleFor()`. Lo reutilizamos como motor de evaluación.
- Este sub-issue no toca ningún otro archivo. Es pure core sin dependencias nuevas.
- El código se mergea con `centralized-security-groups` feature flag = false (no wired todavía).

## Arquitectura

```
pkg/security/               NEW
  groups.go                 NEW — UserGroup, AgentGroup
  policy.go                 NEW — Permission, AgentPolicy, SecurityPolicy
  resolver.go               NEW — ResolveACL()
  security_test.go          NEW
```

### Patron pure core / impure shell

- `pkg/security/` — **puro**: solo tipos y funciones de transformación. Cero I/O, cero side effects.

## Tareas

### Fase 1: Tipos y resolver

- [ ] **1.1** Crear `pkg/security/groups.go`:
  ```go
  type UserGroup struct { Name string; Members []string }
  type AgentGroup struct { Name string; Agents []string }
  ```
- [ ] **1.2** Crear `pkg/security/policy.go`:
  ```go
  type Permission struct { UserGroup string; Actions []string }
  type AgentPolicy struct { AgentGroup string; Permissions []Permission }
  type SecurityPolicy struct { UserGroups []UserGroup; AgentGroups []AgentGroup; Policies []AgentPolicy }
  ```
- [ ] **1.3** Crear `pkg/security/resolver.go` con `ResolveACL(agentID string, p SecurityPolicy) acl.ACL`:
  - Iterar `p.Policies` para encontrar `AgentPolicy` cuyo `AgentGroup` sea un grupo que contenga `agentID` o `"*"`, o sea directamente el `agentID`
  - Para cada `AgentPolicy` que aplique, iterar sus `Permissions`
  - Resolver `Permission.UserGroup` a los `Members` del grupo correspondiente
  - Construir `[]acl.Role` y devolver `acl.ACL` via `acl.FromRoles()` (verificar que esta función existe; si no, añadirla a `pkg/acl/`)
- [ ] **1.4** Soporte wildcard: `AgentGroup.Agents = ["*"]` → aplica la policy a cualquier agentID; `UserGroup.Members = ["*"]` → rol sin restricción de usuario
- [ ] **1.5** Políticas acumulativas: si un agente aparece en múltiples grupos, sus permisos se acumulan (unión de roles)

### Fase 2: Tests

- [ ] **2.1** Test: sin política → ACL vacía (todo permitido, comportamiento actual de acl.Empty())
- [ ] **2.2** Test: agente en grupo → recibe los permisos del grupo
- [ ] **2.3** Test: agente NO en ningún grupo → ACL vacía
- [ ] **2.4** Test: wildcard de agente `"*"` → todos los agentes reciben los permisos
- [ ] **2.5** Test: wildcard de usuario `"*"` → todos los usuarios reciben la acción
- [ ] **2.6** Test: múltiples grupos que incluyen al agente → permisos acumulados (unión)
- [ ] **2.7** Test: agente referenciado directamente por ID en `AgentPolicy.AgentGroup` (sin definir grupo) → recibe permisos

### Fase 3: Cleanup

- [ ] **3.1** `go build -tags goolm ./...` compila sin errores
- [ ] **3.2** `go test -tags goolm ./pkg/security/...` pasa

## Ejemplo de uso

```go
policy := security.SecurityPolicy{
    UserGroups: []security.UserGroup{
        {Name: "admins", Members: []string{"@alice:matrix.org"}},
        {Name: "everyone", Members: []string{"*"}},
    },
    AgentGroups: []security.AgentGroup{
        {Name: "all", Agents: []string{"*"}},
    },
    Policies: []security.AgentPolicy{
        {
            AgentGroup: "all",
            Permissions: []security.Permission{
                {UserGroup: "admins", Actions: []string{"*"}},
                {UserGroup: "everyone", Actions: []string{"ask"}},
            },
        },
    },
}

acl := security.ResolveACL("assistant-bot", policy)
acl.CanDo("@alice:matrix.org", "tool:ssh_command") // true (admin → "*")
acl.CanDo("@unknown:matrix.org", "ask")             // true (everyone → "ask")
acl.CanDo("@unknown:matrix.org", "command:deploy")  // false
```

## Decisiones de diseño

- **No reemplazar pkg/acl/**: este paquete produce `acl.ACL`, no lo sustituye. Máxima reutilización.
- **AgentPolicy.AgentGroup acepta nombre de grupo O ID directo de agente**: permite asignar permisos a un agente individual sin crear un grupo de un solo elemento.
- **Unión de permisos entre grupos**: si un agente está en `assistants` y en `all`, recibe la unión de sus permisos. Seguro: siempre da más acceso, nunca menos de lo esperado.

## Prerequisitos

- `pkg/acl/` compilando (completado en issue 0010)

## Riesgos

- **acl.FromRoles() puede no existir**: si `pkg/acl/` solo expone `FromMap(map[string]RoleDef)`, añadir `FromRoles([]Role) ACL` en ese paquete como parte de esta tarea. Es una adición mínima.
