Files
agents_and_robots/cmd/launcher/main.go
T
egutierrez 8811d45fd1 feat: conectar sistema centralizado de seguridad al launcher y runtime
- 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 <noreply@anthropic.com>
2026-03-08 20:56:21 +00:00

314 lines
8.9 KiB
Go

// Command launcher starts one or more agents from their config files.
//
// Usage:
//
// go run ./cmd/launcher # auto-discovers agents/*/config.yaml
// go run ./cmd/launcher -c agents/assistant/config.yaml
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"maunium.net/go/mautrix"
"github.com/spf13/cobra"
"github.com/enmanuel/agents/agents"
assistantagent "github.com/enmanuel/agents/agents/assistant-bot"
asistente2agent "github.com/enmanuel/agents/agents/asistente-2"
meteorologoagent "github.com/enmanuel/agents/agents/meteorologo"
"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.
// Add a new entry here when you create a new agent package.
var rulesRegistry = map[string]func() []decision.Rule{
"assistant-bot": assistantagent.Rules,
"asistente-2": asistente2agent.Rules,
"meteorologo": meteorologoagent.Rules,
}
func main() {
var (
configPaths []string
logLevel string
logDir string
)
root := &cobra.Command{
Use: "launcher",
Short: "Start Matrix agents from config files",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if len(configPaths) == 0 {
matches, _ := filepath.Glob("agents/*/config.yaml")
configPaths = matches
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
lvl := parseLogLevel(logLevel)
// ── Launcher-level logger ──
logger, launcherCleanup, err := agentlog.NewAgentLogger(agentlog.LoggerConfig{
BaseDir: logDir,
AgentID: "launcher",
Level: lvl,
})
if err != nil {
// Fallback to stdout if file logger fails.
logger = newLogger(logLevel)
logger.Warn("could not create file logger, falling back to stdout", "err", err)
launcherCleanup = func() {}
}
defer launcherCleanup()
if len(configPaths) == 0 {
logger.Warn("no agent configs found — nothing to start")
return nil
}
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)
// ── Start special agents (orchestrator, etc.) BEFORE normal bots ──
orch, err := startOrchestrator(agentBus, logger)
if err != nil {
// Non-fatal: orchestration is optional
logger.Warn("orchestrator not started", "err", err)
} else if orch != nil {
logger.Info("orchestrator initialized")
}
// ── Shared dependencies for agent registry ──
deps := &launchDeps{
agentBus: agentBus,
orch: orch,
logDir: logDir,
logLevel: lvl,
parentCtx: ctx,
secPolicy: secPolicy,
}
registry := newAgentRegistry(deps)
// ── SIGHUP: hot-reload individual agent or all agents ──
sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
go func() {
for {
select {
case <-ctx.Done():
return
case _, ok := <-sighup:
if !ok {
return
}
id := readReloadTarget("run/reload.txt")
// Remove the target file after reading so it doesn't
// affect the next SIGHUP.
_ = os.Remove("run/reload.txt")
if id == "" {
logger.Info("sighup: reloading all agents")
registry.reloadAll(rulesFor)
} else {
logger.Info("sighup: reloading agent", "id", id)
registry.reload(id, rulesFor)
}
}
}
}()
// ── Start normal agents ──
var scannerOnce scanOnce
for _, path := range configPaths {
path := path
cfg, err := config.Load(path)
if err != nil {
logger.Error("failed to load config", "path", path, "err", err)
continue
}
if !cfg.Agent.Enabled {
logger.Info("agent disabled, skipping", "id", cfg.Agent.ID)
continue
}
rules := rulesFor(cfg.Agent.ID, logger)
// Per-agent logger → writes to logs/<agent-id>/YYYY-MM-DD.jsonl
agentLogger, agentCleanup, aErr := agentlog.NewAgentLogger(agentlog.LoggerConfig{
BaseDir: logDir,
AgentID: cfg.Agent.ID,
Level: lvl,
})
if aErr != nil {
logger.Warn("agent file logger failed, using launcher logger", "agent", cfg.Agent.ID, "err", aErr)
agentLogger = logger.With("agent", cfg.Agent.ID)
agentCleanup = func() {}
}
// 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()
continue
}
// Connect agent to bus for orchestration
a.SetBus(agentBus)
// If orchestrator is active, wire interceptor and membership notify
if orch != nil {
a.SetInterceptor(orch.orchestrator.Intercept)
a.SetMembershipNotify(orch.orchestrator.NotifyMembership)
orch.orchestrator.RegisterParticipant(orchestration.ParticipantInfo{
ID: cfg.Agent.ID,
MatrixUserID: cfg.Matrix.UserID,
Description: cfg.Agent.Description,
Capabilities: cfg.Agent.Tags,
})
// Grab the first available Matrix client for room scanning
scannerOnce.set(a.RawMatrixClient())
}
registry.register(&runningAgent{
agent: a,
cfg: cfg,
cfgPath: path,
logger: agentLogger,
logCleanup: agentCleanup,
})
}
// ── Startup room scan (after all participants are registered) ──
if orch != nil && scannerOnce.client != nil {
orch.orchestrator.SetScanner(scannerOnce.client)
scanCtx, scanCancel := context.WithTimeout(ctx, 30*time.Second)
orch.orchestrator.ScanExistingRooms(scanCtx)
scanCancel()
}
registry.waitAll()
registry.cleanupLogs()
logger.Info("all agents stopped")
return nil
},
}
root.Flags().StringSliceVarP(&configPaths, "config", "c", nil,
"Agent config file(s). If omitted, discovers all agents/*/config.yaml")
root.Flags().StringVar(&logLevel, "log-level", "info",
"Log level: debug | info | warn | error")
root.Flags().StringVar(&logDir, "log-dir", "logs",
`Log directory (logs/<agent>/YYYY-MM-DD.jsonl). Use "stdout" for console only`)
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
// scanOnce captures the first Matrix client for room scanning.
type scanOnce struct {
client *mautrix.Client
}
func (s *scanOnce) set(c *mautrix.Client) {
if s.client == nil {
s.client = c
}
}
// orchHandle wraps a running orchestrator with its config for the launcher.
type orchHandle struct {
orchestrator *orchshell.Orchestrator
cfg *config.SpecialConfig
}
// startOrchestrator scans agents/specials/orchestrator/config.yaml and
// initializes the orchestrator if found and enabled.
func startOrchestrator(agentBus *bus.Bus, logger *slog.Logger) (*orchHandle, error) {
cfgPath := filepath.Join("agents", "specials", "orchestrator", "config.yaml")
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
return nil, err
}
cfg, err := config.LoadSpecial(cfgPath)
if err != nil {
return nil, err
}
if !cfg.Special.Enabled {
return nil, nil
}
orchLogger := logger.With("component", "orchestrator")
orch, err := orchshell.New(cfg, agentBus, orchLogger)
if err != nil {
return nil, err
}
return &orchHandle{orchestrator: orch, cfg: cfg}, nil
}
func rulesFor(agentID string, logger *slog.Logger) []decision.Rule {
factory, ok := rulesRegistry[agentID]
if !ok {
logger.Warn("no rules registered for agent, using empty ruleset", "id", agentID)
return nil
}
return factory()
}
func parseLogLevel(level string) slog.Level {
switch level {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
// newLogger creates a stdout-only JSON logger (fallback when file logger fails).
func newLogger(level string) *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: parseLogLevel(level)}))
}