eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// Package agents is the workstation bridge behind `lchat agents run`
2// (docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md): an
3// ordinary E2EE bot member that owns headless Claude Code sessions on the
4// machine it runs on, translating their structured event stream into
5// encrypted session.* events in a group thread (the vocabulary pinned by
6// test/event-vectors/agents.json and folded by web/static/apps/agents.js),
7// and reading members' steering events back off the same thread.
8//
9// Trust model, in one line: the bridge holds this machine's authority, and
10// GROUP MEMBERSHIP IS THE CONSENT MOMENT — everyone in a configured group can
11// steer sessions, so the config below is the blast radius, written down in
12// one place on the box.
13package agents
14
15import (
16 "encoding/json"
17 "fmt"
18 "os"
19 "path/filepath"
20 "strings"
21)
22
23// GroupConfig is one group thread's authority grant.
24type GroupConfig struct {
25 // Roots are the directories scoped sessions may live under (each session
26 // gets a fresh git worktree beside its root). The first root is the
27 // default a conversational spawn lands in.
28 Roots []string `json:"roots"`
29 // Plain opts the group into whole-workstation sessions (cwd = home, no
30 // pinning). Off by default because it hands every member the entire box.
31 Plain bool `json:"plain"`
32}
33
34// Config is the bridge's on-disk config — a sidecar-local JSON beside the
35// session file, following the recipe-file convention (internal/bot/recipe.go):
36// loaded fail-closed at startup, never fetched from the server (the blast
37// radius must live on the box it describes).
38type Config struct {
39 Groups map[string]GroupConfig `json:"groups"`
40 // Command overrides the `claude` binary (tests point this at a stub).
41 Command string `json:"command,omitempty"`
42}
43
44// LoadConfig reads and validates the bridge config. Fail-closed: a missing or
45// broken file is an error, not an empty grant — a bridge with authority over
46// nothing is a misconfiguration someone should see, and a truncated file must
47// never silently widen or narrow what a group can reach.
48func LoadConfig(path string) (*Config, error) {
49 data, err := os.ReadFile(path)
50 if err != nil {
51 return nil, err
52 }
53 var cfg Config
54 if err := json.Unmarshal(data, &cfg); err != nil {
55 return nil, fmt.Errorf("%s: %v", path, err)
56 }
57 if len(cfg.Groups) == 0 {
58 return nil, fmt.Errorf("%s: no groups configured — the bridge would serve nothing", path)
59 }
60 home, _ := os.UserHomeDir()
61 for tid, g := range cfg.Groups {
62 if tid == "" {
63 return nil, fmt.Errorf("%s: empty group thread id", path)
64 }
65 if len(g.Roots) == 0 && !g.Plain {
66 return nil, fmt.Errorf("%s: group %s grants no roots and no plain — it could spawn nothing", path, tid)
67 }
68 roots := make([]string, 0, len(g.Roots))
69 for _, r := range g.Roots {
70 r = strings.TrimSpace(r)
71 if r == "" {
72 return nil, fmt.Errorf("%s: group %s has an empty root", path, tid)
73 }
74 if r == "~" || strings.HasPrefix(r, "~/") {
75 r = filepath.Join(home, strings.TrimPrefix(strings.TrimPrefix(r, "~"), "/"))
76 }
77 abs, err := filepath.Abs(r)
78 if err != nil {
79 return nil, fmt.Errorf("%s: group %s root %q: %v", path, tid, r, err)
80 }
81 st, err := os.Stat(abs)
82 if err != nil {
83 return nil, fmt.Errorf("%s: group %s root %q: %v", path, tid, r, err)
84 }
85 if !st.IsDir() {
86 return nil, fmt.Errorf("%s: group %s root %q is not a directory", path, tid, r)
87 }
88 roots = append(roots, abs)
89 }
90 g.Roots = roots
91 cfg.Groups[tid] = g
92 }
93 if cfg.Command == "" {
94 cfg.Command = "claude"
95 }
96 return &cfg, nil
97}
98