Merge pull request #52 from jacisjake/task/43-boss-nests
feat(boss): Boss creates nests and assigns registry crows (#43)
This commit was merged in pull request #52.
This commit is contained in:
6 files changed
+274
-8
No files matched your search
@@ -117,7 +117,7 @@ A relay (D3, task #40): when the owner's request asks another named member crow
|
|||||||
- The default nest is `todo`, with an empty core: its crows come from the whole flock, so nothing there is ever "borrowed".
|
- The default nest is `todo`, with an empty core: its crows come from the whole flock, so nothing there is ever "borrowed".
|
||||||
- Crows are picked core-first (rotating within the core), then borrowed from outside it (rotating), still capped by the flock guardrails.
|
- Crows are picked core-first (rotating within the core), then borrowed from outside it (rotating), still capped by the flock guardrails.
|
||||||
- Nests persist on the server (`.crows/nests.json`).
|
- Nests persist on the server (`.crows/nests.json`).
|
||||||
- Boss (Chief of Staff crow, CHATROOM_SPEC section 6) is a service role like Elder and Quill: a `request` addressed to it gets one `say` from Boss naming the crow and the outcome. Asked to spin up a crow, Boss answers with a structured order (`SPIN {"name","persona","trust"}`) that the harness executes; everything else gets a plain reply.
|
- Boss (Chief of Staff crow, CHATROOM_SPEC section 6) is a service role like Elder and Quill: a `request` addressed to it gets one `say` from Boss naming the crow and the outcome. Asked to spin up a crow, Boss answers with a structured order (`SPIN {"name","persona","trust"}`) that the harness executes; asked to create a nest, with a `NEST {"name","crows"}` order the harness executes through the nest store — so the harness may create nests via Boss, and a Boss-staffed nest may hold registry (spun) crows as members. No new HTTP endpoint. Anything else gets a plain reply.
|
||||||
|
|
||||||
### Spun crows
|
### Spun crows
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ Crows the Boss creates live in a **crow registry** (`.crows/crows.json`), separa
|
|||||||
| POST | `/api/nests` | `{"name": "Launch copy", "crows": ["Pepper", "Inky"]}` | 201 with the nest; 400 with `{"message"}` if invalid |
|
| POST | `/api/nests` | `{"name": "Launch copy", "crows": ["Pepper", "Inky"]}` | 201 with the nest; 400 with `{"message"}` if invalid |
|
||||||
| POST | `/api/lists` | adds optional `"nest_id"` (default `todo`) | 404 with `{"message"}` for an unknown nest |
|
| POST | `/api/lists` | adds optional `"nest_id"` (default `todo`) | 404 with `{"message"}` for an unknown nest |
|
||||||
|
|
||||||
Guardrails: name 1–40 characters after trimming; `nest_id` is a slug of the name (lowercase, `a-z0-9-`), unique (409 with `{"message"}` on a clash); `crows` must be roster names, no duplicates, 1–8 of them; at most 20 nests.
|
Guardrails: name 1–40 characters after trimming; `nest_id` is a slug of the name (lowercase, `a-z0-9-`), unique (409 with `{"message"}` on a clash); `crows` must be roster or registry (spun) names, no duplicates, 1–8 of them; at most 20 nests.
|
||||||
|
|
||||||
Event changes:
|
Event changes:
|
||||||
- `list.started` adds `nest_id`.
|
- `list.started` adds `nest_id`.
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// bossSystem is the Chief of Staff's contract. A spin-up order comes back on a
|
// bossSystem is the Chief of Staff's contract. A spin-up order comes back on a
|
||||||
// SPIN line the harness reads and executes; anything else is plain talk.
|
// SPIN line and a nest order on a NEST line, both read and executed by the
|
||||||
|
// harness; anything else is plain talk.
|
||||||
const bossSystem = `You are Boss crow, the Chief of Staff of a flock of AI agents. You run the flock's schedule and keep its books.
|
const bossSystem = `You are Boss crow, the Chief of Staff of a flock of AI agents. You run the flock's schedule and keep its books.
|
||||||
When the owner asks you to spin up a new crow, your ENTIRE reply must start with this first line:
|
When the owner asks you to spin up a new crow, your ENTIRE reply must start with this first line:
|
||||||
SPIN {"name":"<name>","persona":"<persona>","trust":"stranger|regular"}
|
SPIN {"name":"<name>","persona":"<persona>","trust":"stranger|regular"}
|
||||||
Pick "persona" from the existing flock roster; a new crow only runs on an existing persona's habits. Starting trust is stranger or regular, never higher.
|
Pick "persona" from the existing flock roster; a new crow only runs on an existing persona's habits. Starting trust is stranger or regular, never higher.
|
||||||
For anything else, reply in plain text. Do not use the SPIN line unless the owner asked for a new crow.`
|
When the owner asks you to create a nest, your ENTIRE reply must start with this first line:
|
||||||
|
NEST {"name":"<nest name>","crows":["<crow>","<crow>"]}
|
||||||
|
The crows are the working flock members the owner named; the Sentry, Elder, Quill and Boss serve every nest and are never members.
|
||||||
|
For anything else, reply in plain text. Do not use the SPIN or NEST line unless the owner asked for a new crow or a new nest.`
|
||||||
|
|
||||||
// spinOrder is the JSON after a "SPIN " prefix.
|
// spinOrder is the JSON after a "SPIN " prefix.
|
||||||
type spinOrder struct {
|
type spinOrder struct {
|
||||||
@@ -22,9 +26,16 @@ type spinOrder struct {
|
|||||||
Trust string `json:"trust"`
|
Trust string `json:"trust"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nestOrder is the JSON after a "NEST " prefix.
|
||||||
|
type nestOrder struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Crows []string `json:"crows"`
|
||||||
|
}
|
||||||
|
|
||||||
// respondBoss answers a request addressed to Boss: one model call through the
|
// respondBoss answers a request addressed to Boss: one model call through the
|
||||||
// flock (CHATROOM_SPEC section 6). A SPIN reply is validated, written to the
|
// flock (CHATROOM_SPEC section 6). A SPIN reply is validated, written to the
|
||||||
// registry, and announced; a refusal says why. No reply posts nothing.
|
// registry, and announced; a NEST reply is validated and built through the
|
||||||
|
// nest store; a refusal says why. No reply posts nothing.
|
||||||
func (s *Server) respondBoss(nest Nest, req Message) {
|
func (s *Server) respondBoss(nest Nest, req Message) {
|
||||||
budget := &Budget{name: "boss-command", limit: s.p.G.TokenBudgetPerItem}
|
budget := &Budget{name: "boss-command", limit: s.p.G.TokenBudgetPerItem}
|
||||||
system := systemFor("Boss", bossSystem, false)
|
system := systemFor("Boss", bossSystem, false)
|
||||||
@@ -34,11 +45,16 @@ func (s *Server) respondBoss(nest Nest, req Message) {
|
|||||||
return // a flock that cannot answer says and does nothing
|
return // a flock that cannot answer says and does nothing
|
||||||
}
|
}
|
||||||
order, isSpin, reason := s.parseSpin(answer)
|
order, isSpin, reason := s.parseSpin(answer)
|
||||||
|
nOrder, isNest, nestReason := s.parseNest(answer)
|
||||||
switch {
|
switch {
|
||||||
case isSpin && reason == "":
|
case isSpin && reason == "":
|
||||||
s.spinCrow(nest, req, order)
|
s.spinCrow(nest, req, order)
|
||||||
case isSpin:
|
case isSpin:
|
||||||
s.postBossSay(nest, req, order.Name, reason)
|
s.postBossSay(nest, req, order.Name, reason)
|
||||||
|
case isNest && nestReason == "":
|
||||||
|
s.createNest(nest, req, nOrder)
|
||||||
|
case isNest:
|
||||||
|
s.postBossSay(nest, req, nOrder.Name, nestReason)
|
||||||
default:
|
default:
|
||||||
s.postBossSay(nest, req, "", strings.TrimSpace(answer))
|
s.postBossSay(nest, req, "", strings.TrimSpace(answer))
|
||||||
}
|
}
|
||||||
@@ -82,6 +98,31 @@ func (s *Server) validPersona(name string) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseNest reads a NEST reply. isNest says the reply was a nest order;
|
||||||
|
// reason, when not "", is why the order never reaches the nest store. Member
|
||||||
|
// names are resolved by the store itself: persona crows and registry crows
|
||||||
|
// pass, anything else (service roles included) is refused there.
|
||||||
|
func (s *Server) parseNest(answer string) (order nestOrder, isNest bool, reason string) {
|
||||||
|
text := strings.TrimSpace(answer)
|
||||||
|
if !strings.HasPrefix(text, "NEST ") {
|
||||||
|
return nestOrder{}, false, ""
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(strings.TrimSpace(strings.TrimPrefix(text, "NEST "))), &order); err != nil {
|
||||||
|
return nestOrder{}, true, "that order did not parse; try again."
|
||||||
|
}
|
||||||
|
order.Name = strings.TrimSpace(order.Name)
|
||||||
|
for i, c := range order.Crows {
|
||||||
|
order.Crows[i] = strings.TrimSpace(c)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case order.Name == "":
|
||||||
|
return order, true, "that order had no nest name in it."
|
||||||
|
case len(order.Crows) == 0:
|
||||||
|
return order, true, "that order had no crows in it."
|
||||||
|
}
|
||||||
|
return order, true, ""
|
||||||
|
}
|
||||||
|
|
||||||
// nameTaken checks the persona roster and the registry alike.
|
// nameTaken checks the persona roster and the registry alike.
|
||||||
func (s *Server) nameTaken(name string) bool {
|
func (s *Server) nameTaken(name string) bool {
|
||||||
if s.validPersona(name) {
|
if s.validPersona(name) {
|
||||||
@@ -108,6 +149,34 @@ func (s *Server) spinCrow(nest Nest, req Message, order spinOrder) {
|
|||||||
s.postBossSay(nest, req, order.Name, fmt.Sprintf("%s joined, running on %s's habits at %s.", order.Name, order.Persona, order.Trust))
|
s.postBossSay(nest, req, order.Name, fmt.Sprintf("%s joined, running on %s's habits at %s.", order.Name, order.Persona, order.Trust))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createNest builds the nest Boss was ordered into. The store has the last
|
||||||
|
// word on guardrails — name, members (persona roster or registry), duplicates
|
||||||
|
// and the nest cap are all checked there before anything mutates, so a bad
|
||||||
|
// order never leaves a partial nest. Success posts a Boss say announcing
|
||||||
|
// membership in the new nest, plus a confirming say in the requesting nest; a
|
||||||
|
// refusal says why.
|
||||||
|
func (s *Server) createNest(reqNest Nest, req Message, order nestOrder) {
|
||||||
|
created, err := s.nests.Create(order.Name, order.Crows)
|
||||||
|
if err != nil {
|
||||||
|
s.postBossSay(reqNest, req, order.Name, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
members := strings.Join(created.Crows, ", ")
|
||||||
|
open := Message{
|
||||||
|
NestID: created.ID,
|
||||||
|
Kind: "say",
|
||||||
|
Author: "Boss",
|
||||||
|
Text: fmt.Sprintf("This nest is open: %s. Its crows: %s.", created.Name, members),
|
||||||
|
To: req.Author,
|
||||||
|
TaggedBy: "harness",
|
||||||
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if record, err := s.msgs.Append(open); err == nil {
|
||||||
|
s.emitNestMessage(record)
|
||||||
|
}
|
||||||
|
s.postBossSay(reqNest, req, created.Name, fmt.Sprintf("%s opened with %s.", created.Name, members))
|
||||||
|
}
|
||||||
|
|
||||||
// postBossSay is Boss's one-line reply: an outcome or a refusal, always a say.
|
// postBossSay is Boss's one-line reply: an outcome or a refusal, always a say.
|
||||||
func (s *Server) postBossSay(nest Nest, req Message, name, text string) {
|
func (s *Server) postBossSay(nest Nest, req Message, name, text string) {
|
||||||
reply := Message{
|
reply := Message{
|
||||||
|
|||||||
+171
@@ -3,9 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -333,3 +335,172 @@ func TestCrowsRegistryEmptyWithoutStore(t *testing.T) {
|
|||||||
t.Fatalf("want no crows, got %v", out.Crows)
|
t.Fatalf("want no crows, got %v", out.Crows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- #43: Boss creates nests and assigns members ---
|
||||||
|
|
||||||
|
// listNests returns every nest from GET /api/nests.
|
||||||
|
func listNests(t *testing.T, ts *httptest.Server) []map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
resp, err := http.Get(ts.URL + "/api/nests")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("GET /api/nests: want 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var out struct {
|
||||||
|
Nests []map[string]any `json:"nests"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return out.Nests
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNest(t *testing.T, ts *httptest.Server, name string) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
for _, n := range listNests(t, ts) {
|
||||||
|
if n["name"] == name {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestCrowNames(nest map[string]any) []string {
|
||||||
|
raw, _ := nest["crows"].([]any)
|
||||||
|
out := make([]string, 0, len(raw))
|
||||||
|
for _, c := range raw {
|
||||||
|
out = append(out, c.(string))
|
||||||
|
}
|
||||||
|
slices.Sort(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBossCreatesNest(t *testing.T) {
|
||||||
|
s, bb := newBossServer(t,
|
||||||
|
`SPIN {"name":"Ash","persona":"Soot","trust":"stranger"}`,
|
||||||
|
`NEST {"name":"Launch","crows":["Hugin","Ash"]}`,
|
||||||
|
)
|
||||||
|
ts := serve(s)
|
||||||
|
defer ts.Close()
|
||||||
|
labID := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
|
||||||
|
// Ash must exist as a registry crow: spin it through Boss first.
|
||||||
|
askBoss(t, ts, labID, "spin up a crow named Ash")
|
||||||
|
bossSay(t, ts, labID, 2)
|
||||||
|
|
||||||
|
askBoss(t, ts, labID, "make a nest Launch with Hugin and Ash")
|
||||||
|
|
||||||
|
var launch map[string]any
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for launch == nil && time.Now().Before(deadline) {
|
||||||
|
launch = findNest(t, ts, "Launch")
|
||||||
|
if launch == nil {
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if launch == nil {
|
||||||
|
t.Fatalf("Boss must create the nest Launch, nests: %v", listNests(t, ts))
|
||||||
|
}
|
||||||
|
if got := nestCrowNames(launch); !slices.Equal(got, []string{"Ash", "Hugin"}) {
|
||||||
|
t.Fatalf("Launch crows must be exactly [Ash Hugin], got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boss announces membership in the NEW nest: a say to the owner.
|
||||||
|
announced := false
|
||||||
|
for _, m := range awaitMessages(t, ts, launch["nest_id"].(string), 1) {
|
||||||
|
if m["author"] == "Boss" && m["kind"] == "say" && m["to"] == "owner" {
|
||||||
|
announced = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !announced {
|
||||||
|
t.Fatalf("Boss must post a say to the owner in Launch, got %v",
|
||||||
|
nestMessages(t, ts, launch["nest_id"].(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// And confirms in the requesting nest.
|
||||||
|
confirmed := false
|
||||||
|
for _, m := range awaitMessages(t, ts, labID, 4) {
|
||||||
|
if m["author"] == "Boss" && m["kind"] == "say" && strings.Contains(m["text"].(string), "Launch") {
|
||||||
|
confirmed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
t.Fatalf("Boss must confirm the new nest in Lab, got %v", nestMessages(t, ts, labID))
|
||||||
|
}
|
||||||
|
if bb.count() != 2 {
|
||||||
|
t.Fatalf("two orders are two model calls, got %d", bb.count())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBossNestUnknownMemberRefused(t *testing.T) {
|
||||||
|
s, _ := newBossServer(t, `NEST {"name":"Nope","crows":["Gandalf"]}`)
|
||||||
|
ts := serve(s)
|
||||||
|
defer ts.Close()
|
||||||
|
labID := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
|
||||||
|
before := len(listNests(t, ts))
|
||||||
|
askBoss(t, ts, labID, "make a nest Nope with Gandalf")
|
||||||
|
|
||||||
|
say := bossSay(t, ts, labID, 2)
|
||||||
|
text, _ := say["text"].(string)
|
||||||
|
if strings.HasPrefix(text, "NEST") {
|
||||||
|
t.Fatalf("Boss must interpret the order, not echo it back: %q", text)
|
||||||
|
}
|
||||||
|
if !strings.Contains(text, "Gandalf") {
|
||||||
|
t.Fatalf("the refusal must name the unknown member, got %q", text)
|
||||||
|
}
|
||||||
|
if after := len(listNests(t, ts)); after != before {
|
||||||
|
t.Fatalf("an unknown member must refuse the whole create: %d before, %d after", before, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBossNestCapRespected(t *testing.T) {
|
||||||
|
s, _ := newBossServer(t, `NEST {"name":"Overflow","crows":["Hugin"]}`)
|
||||||
|
ts := serve(s)
|
||||||
|
defer ts.Close()
|
||||||
|
labID := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
|
||||||
|
// Fill the store to its cap: todo + Lab + (maxNests-2) pads.
|
||||||
|
for i := 0; 2+i < maxNests; i++ {
|
||||||
|
if _, err := s.nests.Create(fmt.Sprintf("Pad %d", i), []string{"Hugin"}); err != nil {
|
||||||
|
t.Fatalf("padding nests: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n := len(listNests(t, ts)); n != maxNests {
|
||||||
|
t.Fatalf("test setup: want %d nests, got %d", maxNests, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
askBoss(t, ts, labID, "make a nest Overflow with Hugin") // 201: no 500 at the front door
|
||||||
|
say := bossSay(t, ts, labID, 2)
|
||||||
|
text, _ := say["text"].(string)
|
||||||
|
if !strings.Contains(text, fmt.Sprintf("at most %d nests", maxNests)) {
|
||||||
|
t.Fatalf("Boss must report the store's cap refusal, got %q", text)
|
||||||
|
}
|
||||||
|
if findNest(t, ts, "Overflow") != nil {
|
||||||
|
t.Fatal("a capped create must not make a nest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBossNestGoesThroughFlock(t *testing.T) {
|
||||||
|
s, _ := newBossServer(t, `NEST {"name":"Launch","crows":["Hugin","Munin"]}`)
|
||||||
|
s.p.Flock.sem = make(chan struct{}, 0) // saturated flock: ask must wait, never act
|
||||||
|
ts := serve(s)
|
||||||
|
defer ts.Close()
|
||||||
|
labID := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
|
||||||
|
before := len(listNests(t, ts))
|
||||||
|
askBoss(t, ts, labID, "make a nest Launch with Hugin and Munin")
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
if n := len(listNests(t, ts)); n != before {
|
||||||
|
t.Fatalf("a saturated flock must not create a nest, got %v", listNests(t, ts))
|
||||||
|
}
|
||||||
|
for _, m := range nestMessages(t, ts, labID) {
|
||||||
|
if m["author"] == "Boss" {
|
||||||
|
t.Fatalf("a saturated flock must not produce a Boss message, got %v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,6 +86,12 @@ func (s *CrowStore) Add(c SpunCrow) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has reports whether a crow with that name is in the registry.
|
||||||
|
func (s *CrowStore) Has(name string) bool {
|
||||||
|
_, ok := s.Get(name)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// Len is how many crows the registry holds.
|
// Len is how many crows the registry holds.
|
||||||
func (s *CrowStore) Len() int {
|
func (s *CrowStore) Len() int {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ type NestStore struct {
|
|||||||
path string
|
path string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
made []Nest // created nests, in creation order; the default nest is not stored
|
made []Nest // created nests, in creation order; the default nest is not stored
|
||||||
|
allowCrow func(name string) bool // extra member names beyond the roster (spun crows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowCrows extends the names that may be nest members beyond the persona
|
||||||
|
// roster: the server wires it to the spun-crow registry so the Boss can
|
||||||
|
// staff a nest with registry crows (#43). It is set once at wiring time,
|
||||||
|
// before anything is served; Create stays the only mutation.
|
||||||
|
func (s *NestStore) AllowCrows(ok func(name string) bool) { s.allowCrow = ok }
|
||||||
|
|
||||||
|
// memberKnown reports whether a name may join a nest: a working persona crow
|
||||||
|
// (the roster excludes the service roles) or an allowed extra name.
|
||||||
|
func (s *NestStore) memberKnown(name string) bool {
|
||||||
|
return slices.Contains(roster, name) || (s.allowCrow != nil && s.allowCrow(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadNests(path string) (*NestStore, error) {
|
func LoadNests(path string) (*NestStore, error) {
|
||||||
@@ -104,7 +117,7 @@ func (s *NestStore) Create(name string, crows []string) (Nest, error) {
|
|||||||
return Nest{}, invalid("A nest has 1 to %d crows.", len(roster))
|
return Nest{}, invalid("A nest has 1 to %d crows.", len(roster))
|
||||||
}
|
}
|
||||||
for i, c := range crows {
|
for i, c := range crows {
|
||||||
if !slices.Contains(roster, c) {
|
if !s.memberKnown(c) {
|
||||||
return Nest{}, invalid("%q is not in the flock. Choose from %s.", c, strings.Join(roster, ", "))
|
return Nest{}, invalid("%q is not in the flock. Choose from %s.", c, strings.Join(roster, ", "))
|
||||||
}
|
}
|
||||||
if slices.Contains(crows[:i], c) {
|
if slices.Contains(crows[:i], c) {
|
||||||
|
|||||||
@@ -46,8 +46,15 @@ func NewServer(ctx context.Context, p *Pipeline, approver *WebApprover, nests *N
|
|||||||
// WithMessages attaches the nest conversation log (chatroom path).
|
// WithMessages attaches the nest conversation log (chatroom path).
|
||||||
func (s *Server) WithMessages(m *MessageStore) *Server { s.msgs = m; return s }
|
func (s *Server) WithMessages(m *MessageStore) *Server { s.msgs = m; return s }
|
||||||
|
|
||||||
// WithCrows attaches the spun-crow registry (the Boss writes through it).
|
// WithCrows attaches the spun-crow registry (the Boss writes through it) and
|
||||||
func (s *Server) WithCrows(c *CrowStore) *Server { s.crows = c; return s }
|
// lets registry crows be nest members (#43).
|
||||||
|
func (s *Server) WithCrows(c *CrowStore) *Server {
|
||||||
|
s.crows = c
|
||||||
|
if c != nil && s.nests != nil {
|
||||||
|
s.nests.AllowCrows(c.Has)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Handler() http.Handler {
|
func (s *Server) Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|||||||
Reference in new issue
Block a user