give: take only a value a person holds, ask the desk by name, let the controller alone ask it, and announce every value given

The review of 2026-10-09 (M4):
- give refuses broker (the bus account issue mints) and any own secret the mesh may make itself;
- the desk's prompt is asked by module, secret and machine, never with words of the caller's, and the
  bus denies the prompt's subjects to every principal but the controller, however wide its grant, so the
  prompt's 'the controller asks' is the bus's word (Permissions.PublishDeny, broker.ControllerOnly);
- secret accept with a value is refused through a verb: a value comes from the terminal or the desk;
- every value given for an own secret, at the terminal or the desk, raises the urgent condition
  secret-given on every channel, until the operator silences it.
This commit is contained in:
2026-10-09 16:22:47 +02:00
parent 5689553406
commit be59f29f46
7 changed files with 250 additions and 13 deletions
+43 -3
View File
@@ -10,6 +10,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/secrets"
)
@@ -44,6 +45,9 @@ type deskGive struct {
accept func(value string) (untilStart bool, err error)
// record writes the act in the hand-act log.
record func(link.HandAct) error
// announce raises the condition that says a module's own secret was given (secretGivenObservation), on
// every channel; nil announces nothing (a test that does not look).
announce func(node, module, name, how string) error
}
// errNothingGiven is a prompt dismissed, or not answered in time: nothing changes.
@@ -63,10 +67,12 @@ func (d deskGive) give(node, module, name, desk string) (string, error) {
if err != nil {
return "", fmt.Errorf("no key could be made to take the value: %w", err)
}
// By name, never by words: the holder writes the prompt from these, and says the controller asks, which
// the bus alone makes true (broker.ControllerOnly).
raw, err := d.ask(desk, map[string]any{
"prompt": name + " for " + module,
"message": fmt.Sprintf("The mesh asks for %s, the own secret of %s on %s. What you type is not shown, "+
"and is sealed before it leaves this machine. Type it only if you asked for this.", name, module, node),
"module": module,
"secret": name,
"node": node,
"seal_to": public,
"timeout_seconds": deskPromptWithin,
})
@@ -113,6 +119,11 @@ func (d deskGive) give(node, module, name, desk string) (string, error) {
if err := d.record(act); err != nil {
recorded = fmt.Sprintf("\n this act could NOT be recorded in the hand-act log, and is done anyway: %v", err)
}
if d.announce != nil {
if err := d.announce(node, module, name, "at the desk on "+desk); err != nil {
recorded += fmt.Sprintf("\n this change could NOT be announced on the operator's channels: %v", err)
}
}
words := fmt.Sprintf("%s on %s now holds %q, given at the desk on %s and sealed to %s; the mesh cannot read it "+
"back.\n run `push %s` to send it", module, node, name, desk, node, node)
if untilStart {
@@ -157,6 +168,7 @@ func giveAtDesk(ctx context.Context, node, module, name, desk string) error {
return err
})
},
announce: func(node, module, name, how string) error { return announceSecretGiven(ctx, node, module, name, how) },
}
words, err := d.give(node, module, name, desk)
if err != nil {
@@ -165,3 +177,31 @@ func giveAtDesk(ctx context.Context, node, module, name, desk string) error {
fmt.Println(words)
return nil
}
// kindSecretGiven is the condition every value given for a module's own secret raises (the review of 2026-10-09,
// M4): on every channel, so a bot token changed by somebody else — a channel that now answers for them — is
// heard of. It stays until the operator silences or clears it.
const kindSecretGiven = "secret-given"
// secretGivenObservation is that condition: which secret, of which module on which machine, how and when.
func secretGivenObservation(node, module, name, how string, at time.Time) conditions.Observation {
key := node + "." + module + "." + name
return conditions.Observation{Scope: conditions.ScopeMachine, ID: key, Token: kindSecretGiven, Kind: kindSecretGiven,
Machine: node, Severity: conditions.Urgent, Source: kindSecretGiven,
Summary: fmt.Sprintf("%s of %s on %s was given %s at %s", name, module, node, how,
at.Local().Format("2006-01-02 15:04")),
Headline: "Secret of " + module + " changed",
Explanation: fmt.Sprintf("The secret %s of %s on %s was given %s at %s. If you did not do this, "+
"somebody else holds what %s acts with.", name, module, node, how, at.Local().Format("15:04"), module),
Needs: "silence this if you gave it; if you did not, give the secret again yourself and unlink what it serves.",
Resolved: "You saw that " + name + " of " + module + " was changed",
Actions: []conditions.Action{conditions.SilenceAction(conditions.Key(conditions.ScopeMachine, key, kindSecretGiven))}}
}
// announceSecretGiven raises it on this controller's keeper.
func announceSecretGiven(ctx context.Context, node, module, name, how string) error {
return withKeeper(ctx, func(k *conditions.Keeper) error {
_, err := k.Observe(ctx, secretGivenObservation(node, module, name, how, time.Now()))
return err
})
}
+87
View File
@@ -1,12 +1,15 @@
package main
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/secrets"
)
@@ -139,3 +142,87 @@ func TestTheGiveVerbRunsTheDeskPathAndTheControllerMayAskTheDesk(t *testing.T) {
t.Error("the controller may not ask the desk's prompt")
}
}
// The review of 2026-10-09 (M4): the desk's prompt says who asks in words the caller does not choose — the
// controller, which the bus alone lets ask it — and what for, from names the controller checked; the prompt
// carries no free text of the caller's.
func TestThePromptIsAskedByNameNeverByWordsTheCallerChose(t *testing.T) {
d, _, _, asked := aDesk(t, sealedTo(t, typed))
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
t.Fatal(err)
}
p := (*asked)[0]
if p["module"] != "telegram" || p["secret"] != "telegram-token" || p["node"] != "anchor" {
t.Errorf("the prompt was not asked by name: %v", p)
}
for _, free := range []string{"prompt", "message"} {
if _, there := p[free]; there {
t.Errorf("the prompt carries the caller's %s: %v", free, p)
}
}
}
// Every value given for a module's own secret is announced as a condition, on every channel (the review of
// 2026-10-09, M4): a bot token changed by somebody else is a channel that now answers for them.
func TestAValueGivenAtTheDeskIsAnnounced(t *testing.T) {
d, _, _, _ := aDesk(t, sealedTo(t, typed))
var said []string
d.announce = func(node, module, name, how string) error {
said = append(said, node+" "+module+" "+name+" "+how)
return nil
}
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
t.Fatal(err)
}
if len(said) != 1 || !strings.Contains(said[0], "anchor telegram telegram-token") || !strings.Contains(said[0], "laptop") {
t.Fatalf("announced %v", said)
}
o := secretGivenObservation("anchor", "telegram", "telegram-token", "at the desk on laptop", time.Date(2026, 10, 9, 12, 3, 0, 0, time.UTC))
if o.Severity != conditions.Urgent || !strings.Contains(o.Explanation, "telegram-token") ||
len(o.Actions) == 0 || o.Key() == "" {
t.Errorf("the announcement %+v", o)
}
if strings.Contains(o.Summary+o.Explanation+o.Said, typed) {
t.Error("the announcement carries the value")
}
}
// The bus lets the controller alone ask the desk's prompt (the review of 2026-10-09, M4): the runtime, which
// carries every agent's calls, and a person granted every tool are denied it, however wide their grant.
func TestOnlyTheControllerMayAskTheDesksPrompt(t *testing.T) {
for _, p := range []broker.Principal{
{Kind: broker.KindNodeTools, Node: "laptop"},
{Kind: broker.KindPerson, Module: "operator", Invokes: []string{"*"}},
{Kind: broker.KindModule, Node: "laptop", Module: "lab", Invokes: []string{"seat:node-launcher.secret"}},
} {
perms, err := broker.PermissionsFor(p)
if err != nil {
t.Fatal(err)
}
for _, subject := range []string{"mesh.seat.node-launcher.tool.secret.laptop", "mesh.seat.node-launcher.tool.secret",
"mesh.mod.rofi.tool.node-launcher.secret", "mesh.mod.rofi.tool.node-launcher.secret.laptop"} {
if broker.MayPublish(perms, subject) {
t.Errorf("%s may publish %s", p.Username(), subject)
}
}
}
perms, _ := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
if !broker.MayPublish(perms, "mesh.seat.node-launcher.tool.secret.laptop") {
t.Error("the controller may not ask the desk's prompt")
}
}
// A value for a secret comes from the terminal or the desk, never through a verb (the review of 2026-10-09,
// M4): `secret accept` with a value, run for a verb, is refused before anything is read.
func TestASecretValueIsNeverAcceptedThroughAVerb(t *testing.T) {
t.Setenv(verbVar, "mesh-controller.command")
for _, args := range [][]string{
{"accept", "anchor", "telegram", "telegram-token", "--from", "/dev/null"},
{"accept", "anchor", "app", "db", "--from", "/dev/null", "--provider", "store"},
} {
err := secretCommand(context.Background(), args)
if err == nil || !strings.Contains(err.Error(), "never through a verb") {
t.Errorf("%v: %v", args, err)
}
}
}
+9
View File
@@ -68,6 +68,12 @@ func secretCommand(ctx context.Context, args []string) error {
return errors.New(secretUsage)
}
node, module, name := rest[0], rest[1], rest[2]
// A value comes from the terminal or the desk, never through a verb (the review of 2026-10-09, M4): a
// verb's caller may be an agent, and a value it chose would become what a module acts with.
if verb, through := throughAVerb(); through && *desk == "" {
return fmt.Errorf("a secret's value is given at the controller's terminal or at the desk (`give`), never "+
"through a verb (this line came through %q): nothing was read or sealed", verb)
}
if *desk != "" {
if *from != "" || *provider != "" {
return errors.New("--at-desk gives a module's own secret, and takes neither --from nor --provider")
@@ -106,6 +112,9 @@ func secretCommand(ctx context.Context, args []string) error {
if err != nil {
return err
}
if err := announceSecretGiven(ctx, node, module, name, "at the controller's terminal"); err != nil {
fmt.Printf(" this change could NOT be announced on the operator's channels: %v\n", err)
}
// Not printed back, and there is nowhere it could be printed from: it is sealed to that
// machine and the mesh cannot read it again.
fmt.Printf("%s on %s now holds %q, sealed to that machine.\n", module, node, name)
+54 -5
View File
@@ -187,6 +187,35 @@ var VerbsTheBusStepAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}}
// desk opens a prompt that does not show what is typed, and answers it sealed to the controller's call.
var VerbsTheControllerAsksForASecret = []SeatVerb{{Seat: "node-launcher", Verb: "secret"}}
// ControllerOnly are the subjects the controller alone may publish, however wide another's grant (the review
// of 2026-10-09, M4): the desk's hidden prompt, on its seat's subjects and on any holder's own module
// subjects. A grant of every tool — the runtime's, which carries every agent's calls, or a person's `*` — would
// otherwise reach it, and the prompt says the controller asks: only the bus makes that true.
func ControllerOnly() []string {
var out []string
for _, v := range VerbsTheControllerAsksForASecret {
for _, base := range []string{"mesh.seat." + v.Seat + ".tool." + v.Verb, "mesh.mod.*.tool." + v.Seat + "." + v.Verb} {
out = append(out, base, base+".*")
}
}
return out
}
// MayPublish says whether permissions let a principal publish one subject: an allow covers it and no deny does.
func MayPublish(perms Permissions, subject string) bool {
for _, d := range perms.PublishDeny {
if SubjectsOverlap(d, subject) {
return false
}
}
for _, a := range perms.Publish {
if SubjectsOverlap(a, subject) {
return true
}
}
return false
}
// VerbsTheControllerAsksTheDeliveryOwner are the mesh-delivery seat's verbs the controller calls (novox/hq
// ADR 0239): its self-check reads `stalled`, and healer H2 takes the one transition the table allows
// through `close`. A mesh seat's verb is flat: no machine in the subject.
@@ -257,8 +286,11 @@ func (p Principal) inbox() string { return "_INBOX." + p.Username() + ".>" }
// Permissions is what a principal may publish and subscribe, and whether it may answer.
type Permissions struct {
Publish []string
Subscribe []string
Publish []string
// PublishDeny are subjects refused although an allow covers them: the controller's alone (ControllerOnly),
// denied to everybody whose grant is wide enough to reach them. The server's deny outranks its allow.
PublishDeny []string
Subscribe []string
// AllowResponses lets a principal reply to a request it received, on the reply subject that
// request carried, once.
//
@@ -804,9 +836,22 @@ func PermissionsFor(p Principal) (Permissions, error) {
if err := CheckWriters(p, pub); err != nil {
return Permissions{}, err
}
// What the controller alone may publish is denied to everybody else whose grant reaches it.
var deny []string
if p.Kind != KindController {
for _, only := range ControllerOnly() {
for _, a := range pub {
if SubjectsOverlap(a, only) {
deny = append(deny, only)
break
}
}
}
}
return Permissions{
Publish: pub,
Subscribe: sub,
Publish: pub,
PublishDeny: deny,
Subscribe: sub,
// A module answers what it was asked — a tool call reaches it on its own namespace, so the
// authority is bounded by having been asked — and so does the controller. A node and a
// person are never asked anything, and are granted nothing here.
@@ -1028,7 +1073,11 @@ func ComposeAccounts(principals []Principal) (string, error) {
return "", fmt.Errorf("%s has no password hash: a user without one is a user anybody is", p.Username())
}
fmt.Fprintf(&b, " { user: %q, password: %q, permissions: {\n", p.Username(), p.PasswordHash)
fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quoted(perms.Publish))
if len(perms.PublishDeny) > 0 {
fmt.Fprintf(&b, " publish: { allow: [%s], deny: [%s] }\n", quoted(perms.Publish), quoted(perms.PublishDeny))
} else {
fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quoted(perms.Publish))
}
fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quoted(perms.Subscribe))
if perms.AllowResponses {
fmt.Fprintf(&b, " allow_responses: { max: 1, ttl: \"%dm\" }\n", int(ResponseTTL/time.Minute))
+7 -3
View File
@@ -84,12 +84,16 @@ func graphicalSessionSeats() []Seat {
{Name: "secret", Optional: true, Description: "Ask the operator for a value in a prompt that " +
"does not show what is typed, and answer it sealed to the key the asker gives — never in " +
"the clear — or cancelled when the prompt was dismissed or not answered in time.",
// By name, never by words (the review of 2026-10-09, M4): the holder writes the prompt from the
// module, the secret and the machine, and says the controller asks — the bus lets nobody else
// ask it (broker.ControllerOnly) — so no caller puts words of its own before the operator.
Input: schema(map[string]string{
"prompt": "what the prompt asks",
"message": "a line saying who asks and for what (optional)",
"module": "the module whose own secret is asked for",
"secret": "the own secret's name",
"node": "the machine the module runs on",
"seal_to": "the asker's public sealing key: the answer is sealed to it",
"timeout_seconds": "give up after this long (optional)",
}, []string{"prompt", "seal_to"})},
}, []string{"module", "secret", "node", "seal_to"})},
}},
{Name: NotifierSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{
{Name: "send", Description: "Show the operator a notification.",
+28
View File
@@ -0,0 +1,28 @@
package inventory
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// `give` takes only a value nobody but a person has (the review of 2026-10-09, M4): never the module's bus
// account, which `issue` mints, nor a secret the mesh may make itself.
func TestOnlyASecretThePersonHoldsIsGivenAtTheDesk(t *testing.T) {
m := catalogue.Manifest{Module: "telegram", OwnSecrets: catalogue.OwnSecrets{
"telegram-token": {Path: "/s/telegram-token"},
"broker": {Path: "/s/broker"},
"session": {Path: "/s/session", Taken: catalogue.TakenAtStart},
}}
if err := GivableAtDesk(m, "telegram-token"); err != nil {
t.Errorf("the bot token was refused: %v", err)
}
for _, name := range []string{"broker", "session", "chat-id"} {
if err := GivableAtDesk(m, name); err == nil {
t.Errorf("%s was givable", name)
} else if name != "chat-id" && !strings.Contains(err.Error(), "the mesh makes") {
t.Errorf("%s: %v", name, err)
}
}
}
+22 -2
View File
@@ -561,8 +561,28 @@ func (i *Inventory) DeclaresOwnSecret(ctx context.Context, module, name string)
if err != nil {
return err
}
if _, ok := m.OwnSecrets[name]; !ok {
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
return GivableAtDesk(m, name)
}
// BrokerSecret is the own secret that is a module's bus account, which `issue` mints.
const BrokerSecret = "broker"
// GivableAtDesk refuses, in words, an own secret a module does not declare, and one the mesh makes itself
// (the review of 2026-10-09, M4): the module's bus account, which `issue` mints, and any the mesh may make
// in place of a value given (catalogue.OwnSecret.MeshMayMake). The desk takes only what a person holds and
// the mesh cannot make — a bot's token — so nobody is asked to type the mesh's own credential into a prompt.
func GivableAtDesk(m catalogue.Manifest, name string) error {
own, ok := m.OwnSecrets[name]
if !ok {
return fmt.Errorf("%s does not declare %q as an own secret; %s", m.Module, name, declaresOwn(m))
}
switch {
case name == BrokerSecret:
return fmt.Errorf("%q is %s's account on the bus, which the mesh makes (`issue`), never a value a person gives",
name, m.Module)
case own.MeshMayMake():
return fmt.Errorf("%q of %s is a secret the mesh makes itself (it may replace a value given at the module's "+
"start, ADR 0228); a person gives it only at the controller's terminal, with `secret accept`", name, m.Module)
}
return nil
}