package main import ( "encoding/json" "errors" "fmt" "strings" "sync" "testing" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/secrets" ) // The controller's secret verbs reach the vault only for a module filled the new way (design 57's V2, issue 675, // the release lead's decision on it; novox/hq ADR 0349 clauses 8 to 10 and 17). // standInVault answers the vault's two-step verbs as mesh-vault does: a key made for each call, the value opened // with it here and nowhere else. It keeps what it took and every argument it was sent, as the bus carried them. type standInVault struct { t *testing.T mu sync.Mutex keys map[string]string // call → private key taken map[string]string // secret → value sent []string // every argument, as JSON refuse error // refuses every first step when set outcome []string again bool // the value is held already // perHolder answers `issued` with a value per holder named, the last one not answered (issue 749). perHolder bool // asked is how the vault's own ask at a desk ends, as mesh-vault answers `secret-ask` (issue 723): the vault // asks the desk itself, so the controller sends one step and is told how it ended. asked string } func newStandInVault(t *testing.T) *standInVault { return &standInVault{t: t, keys: map[string]string{}, taken: map[string]string{}} } func (v *standInVault) ask(verb string, args map[string]any) (json.RawMessage, error) { v.mu.Lock() defer v.mu.Unlock() raw, _ := json.Marshal(map[string]any{"verb": verb, "args": args}) v.sent = append(v.sent, string(raw)) if verb == "rotate" { return json.Marshal(vaultChanged{Name: "own/g14/dunst/token", Generation: 2, By: fmt.Sprint(args["by"])}) } // The vault asks the desk's prompt and the provider's tool itself, in its own name (issue 723): one step from // the controller, which carries no key and no sealed value. switch verb { case "secret-ask": if v.refuse != nil { return nil, v.refuse } outcome := v.asked if outcome == "" { outcome = "given" } return json.Marshal(vaultAsked{Secret: fmt.Sprintf("own/%v/%v/%v", args["node"], args["module"], args["name"]), Generation: 1, AskedBy: "as-the-vault-recorded", Outcome: outcome}) case "issued": if v.refuse != nil { return nil, v.refuse } // A tool answering a value per holder (issue 749): the vault keeps one for each holder named, and says which // it was not given. if holders, ok := args["holders"].([]issuedHolder); ok && v.perHolder { out := vaultIssued{Secret: fmt.Sprintf("provision/%v/%v", args["provider"], args["provision"]), Unchanged: v.again} for i, h := range holders { if i == len(holders)-1 && len(holders) > 1 { out.NotAnswered = append(out.NotAnswered, h) continue } out.Holders = append(out.Holders, vaultIssuedHolder{Node: h.Node, Module: h.Module, Secret: fmt.Sprintf("%s/%s/%s", out.Secret, h.Node, h.Module), Generation: 1, Unchanged: v.again}) } return json.Marshal(out) } return json.Marshal(vaultIssued{Secret: fmt.Sprintf("provision/%v/%v", args["provider"], args["provision"]), Generation: 1, Unchanged: v.again}) } call, _ := args["call"].(string) if call == "" { if v.refuse != nil { return nil, v.refuse } public, private, err := secrets.Keypair() if err != nil { v.t.Fatal(err) } call = fmt.Sprintf("call-%d", len(v.keys)+1) v.keys[call] = private secret := fmt.Sprintf("%s/%v", verb, args["name"]) return json.Marshal(vaultOpened{Call: call, SealTo: public, Secret: secret, AskedBy: "as-the-vault-recorded"}) } private, ok := v.keys[call] if !ok { return nil, errors.New("no such call") } delete(v.keys, call) if o, _ := args["outcome"].(string); o != "" { v.outcome = append(v.outcome, o) return json.Marshal(vaultChanged{Unchanged: true}) } value, err := secrets.Open(private, fmt.Sprint(args["sealed"])) if err != nil { return nil, errors.New("not sealed to this call") } // As mesh-vault takes a typed value: its line ending off (intake.go AskEnd). v.taken[verb] = strings.TrimRight(string(value), "\r\n") return json.Marshal(vaultChanged{Name: verb, Generation: 1, Unchanged: v.again}) } func (v *standInVault) said() string { v.mu.Lock() defer v.mu.Unlock() return strings.Join(v.sent, "\n") } // A module's secrets are the vault's once the catalogue marks it moved (design 57 V4, issue 647): its machine's // client asks the vault for them. Every module not marked, and one the catalogue does not hold, keeps today's path. // A switch that cannot be read does nothing, and says why: never today's path by default. func TestASwitchThatCannotBeReadDoesNothing(t *testing.T) { var ran []string err := bySwitch("dunst", func(string) (bool, error) { return false, errors.New("the store is gone") }, func() error { ran = append(ran, "today"); return nil }, func() error { ran = append(ran, "vault"); return nil }) if err == nil || len(ran) > 0 || !strings.Contains(err.Error(), "the store is gone") { t.Errorf("an unreadable switch ran %v (%v)", ran, err) } } // The switch: today's path for a module filled the old way, the vault's for one filled the new way, and never // both. func TestTheSwitchRunsOnePathByTheModulesForm(t *testing.T) { moved := switchOf(func(m string) bool { return m == "dunst" }) for module, want := range map[string]string{"telegram": "today", "dunst": "vault"} { var ran []string err := bySwitch(module, moved, func() error { ran = append(ran, "today"); return nil }, func() error { ran = append(ran, "vault"); return nil }) if err != nil || strings.Join(ran, ",") != want { t.Errorf("%s ran %v (%v), not %s", module, ran, err, want) } } var ran []string _ = bySwitch("dunst", nil, func() error { ran = append(ran, "today"); return nil }, func() error { ran = append(ran, "vault"); return nil }) if strings.Join(ran, ",") != "today" { t.Errorf("no switch ran %v", ran) } } // `secret accept` for a module filled the new way: the value read at the terminal is sealed there to the key the // vault made for this one call; no clear value crosses the bus or reaches the controller's log. Over a real bus, // with a tap on every subject. // The desk's path for a module filled the new way (issue 723): the controller asks the vault once; the vault // records the ask, holds its bounds and asks the desk's prompt itself, so the controller never reaches the desk and // carries no key and no sealed value; today's store is not touched. For a module filled the old way, the vault is // never asked. func TestAnAskForAModuleFilledTheNewWayIsTheVaults(t *testing.T) { d, accepted, acts, asked := aDesk(t, sealedTo(t, typed)) d.askedBy = "g14/claude-code" d.open = func(node, module, name, desk string) (int64, error) { t.Fatal("today's record was asked") return 0, nil } vault := newStandInVault(t) d.vault = vault.ask words, err := d.give("anchor", "telegram", "telegram-token", "laptop") if err != nil { t.Fatal(err) } if len(*accepted) != 0 { t.Fatal("the controller opened and kept what was typed") } if len(*asked) != 0 { t.Fatalf("the controller asked the desk's prompt itself: %v", *asked) } said := vault.said() if !strings.Contains(said, `"verb":"secret-ask"`) || !strings.Contains(said, `"asked_by":"g14/claude-code"`) || !strings.Contains(said, `"desk":"laptop"`) || strings.Count(said, "\n") != 0 { t.Errorf("the vault was not asked once, naming who asked and the desk: %s", said) } if strings.Contains(said, "seal_to") || strings.Contains(said, "sealed") || strings.Contains(said, `"call"`) { t.Errorf("the controller carried a key or a sealed value: %s", said) } if len(*acts) != 1 || (*acts)[0].Cause != "given-at-the-desk" || !strings.Contains((*acts)[0].Why, "kept in the vault") || !strings.Contains((*acts)[0].Why, "as-the-vault-recorded") { t.Errorf("the act: %+v", *acts) } if !strings.Contains(words, "own/anchor/telegram/telegram-token") || strings.Contains(words, typed) { t.Errorf("said %q", words) } // Dismissed and timed out: nothing given, said so. for _, outcome := range []string{"dismissed", "timed-out", "empty"} { d2, _, _, _ := aDesk(t, sealedTo(t, typed)) v2 := newStandInVault(t) v2.asked = outcome d2.vault = v2.ask if _, err := d2.give("anchor", "telegram", "telegram-token", "laptop"); !errors.Is(err, errNothingGiven) { t.Errorf("%s: %v", outcome, err) } } // Failed at the desk: an error, not "nothing given". d5, _, acts5, _ := aDesk(t, sealedTo(t, typed)) v5 := newStandInVault(t) v5.asked = "failed" d5.vault = v5.ask if _, err := d5.give("anchor", "telegram", "telegram-token", "laptop"); err == nil || errors.Is(err, errNothingGiven) || len(*acts5) != 0 { t.Errorf("a failed ask: %v, acts %d", err, len(*acts5)) } // The vault's bounds refuse: said so, nobody is asked by the controller. d3, _, _, asked3 := aDesk(t, sealedTo(t, typed)) v3 := newStandInVault(t) v3.refuse = errors.New("an ask for telegram-token of telegram on anchor is still open") d3.vault = v3.ask if _, err := d3.give("anchor", "telegram", "telegram-token", "laptop"); err == nil || !strings.Contains(err.Error(), "still open") || len(*asked3) != 0 { t.Errorf("a refused ask: %v, prompts %d", err, len(*asked3)) } // Filled the old way: today's path, the vault never asked. d4, accepted4, _, _ := aDesk(t, sealedTo(t, typed)) if _, err := d4.give("anchor", "telegram", "telegram-token", "laptop"); err != nil { t.Fatal(err) } if len(*accepted4) != 1 || d4.vault != nil { t.Error("a module filled the old way did not take today's path") } } // `secret issued` for a provider filled the new way (issue 723): the controller asks the vault once, naming the // provider's machine, module and tool; the vault asks the tool itself; the controller never reaches the tool and // nothing is kept, sealed or sent here. A provider filled the old way keeps today's path and the vault is never // asked. // `secret issued` names to the vault the provision's holders from that provider (issue 749): a tool may answer a // value for each, which the vault keeps as that holder's own; the controller says what was kept for whom and which // holder the tool gave nothing for, never a value. A provider answering one value is told the holders all the same // and keeps the provision's one credential. // The holders named are the provision's holders from that provider, each consuming module on each machine once. func TestTheHoldersNamedAreThoseOfThatProvider(t *testing.T) { holders := []inventory.Holder{ {Provision: "gitea-read", Consumer: "anchor", ConsumerModule: "claude-code", Provider: "novox"}, {Provision: "gitea-read", Consumer: "anchor", ConsumerModule: "claude-code", Local: "second", Provider: "novox"}, {Provision: "gitea-read", Consumer: "laptop", ConsumerModule: "claude-code", Provider: "novox"}, {Provision: "gitea-read", Consumer: "ace", ConsumerModule: "claude-code", Provider: "shanks"}, } got := issuedHoldersFrom(holders, "novox") if fmt.Sprint(got) != "[claude-code on anchor claude-code on laptop]" { t.Errorf("the holders named: %v", got) } if got := issuedHoldersFrom(holders, "g14"); len(got) != 0 { t.Errorf("a provider holding nothing named %v", got) } shelf := map[string]catalogue.Manifest{"gitea": {Module: "gitea", Provides: []catalogue.Offer{{Name: "gitea-read", Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "gitea_agent_token"}}}}} providers, err := issuedProvidersFrom(shelf, holders, func(string) ([]string, error) { return []string{"gitea"}, nil }, "gitea-read") if err != nil || len(providers) != 2 || providers[0].Node != "novox" || fmt.Sprint(providers[0].Holders) != "[claude-code on anchor claude-code on laptop]" || fmt.Sprint(providers[1].Holders) != "[claude-code on ace]" { t.Errorf("the providers asked: %+v %v", providers, err) } } // `secret rotate` for a module filled the new way asks the vault, naming who asked; nothing is sent. func TestARotationForAModuleFilledTheNewWayIsTheVaults(t *testing.T) { vault := newStandInVault(t) if _, err := rotateInVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), "g14/claude-code", " "); err == nil || vault.said() != "" { t.Fatalf("a rotation without a why reached the vault: %v", err) } words, err := rotateInVault(vault.ask, ownSecretArgs("g14", "dunst", "token"), "g14/claude-code", "leaked") if err != nil { t.Fatal(err) } if !strings.Contains(vault.said(), `"verb":"rotate"`) || !strings.Contains(vault.said(), `"by":"g14/claude-code"`) || !strings.Contains(vault.said(), `"module":"dunst"`) || !strings.Contains(vault.said(), `"why":"leaked"`) { t.Errorf("the vault was told %s", vault.said()) } if !strings.Contains(words, "generation 2") || !strings.Contains(words, "takes it from the vault") { t.Errorf("%q", words) } } // The module that decides `accept --provider` is the one on the provider's machine that offers the provision; none, // or several, is none, and today's path then says what is wrong. func TestTheProvidersModuleIsTheOneThatOffersTheProvision(t *testing.T) { offer := func(module string, provisions ...string) catalogue.Manifest { m := catalogue.Manifest{Module: module} for _, p := range provisions { m.Provides = append(m.Provides, catalogue.Offer{Name: p}) } return m } shelf := map[string]catalogue.Manifest{"tv": offer("tv", "tv-api"), "plex": offer("plex", "plex-api"), "other-tv": offer("other-tv", "tv-api"), "dunst": offer("dunst")} for _, c := range []struct { assigned []string want string }{ {[]string{"dunst", "tv", "plex"}, "tv"}, {[]string{"dunst", "plex"}, ""}, {[]string{"tv", "other-tv"}, ""}, } { if got := offeredOn(shelf, c.assigned, "tv-api"); got != c.want { t.Errorf("%v: %q, want %q", c.assigned, got, c.want) } } }