mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.55s)
mesh/delivery superseded: a newer head of the same pull request
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.
208 lines
9.0 KiB
Go
208 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// A module's own secret given at the operator's desk (novox/hq ADR 0259 §10).
|
|
//
|
|
// **The value never passes through whoever asked for it.** An agent, or the operator at the mesh MCP
|
|
// server, calls `give` with the machine, the module, the secret's name and the desk — never a value. The
|
|
// controller makes a sealing keypair for this one call, asks the desk's `node-launcher.secret` to prompt the
|
|
// operator without showing what is typed, and is answered with what was typed **sealed to that key**: no
|
|
// plaintext on the bus, in a runtime's log or in any call's record. It opens it here, seals it to the
|
|
// module's machine exactly as `secret accept` does, and forgets it. What it answers says only that the
|
|
// value was taken, or why not.
|
|
//
|
|
// **What remains** (ADR 0234's accepted residual risk): on an X11 desk any program of the operator's
|
|
// account can read the keys as they are typed. And a program that calls the desk's prompt itself, with a
|
|
// key of its own, is answered with what the operator typed into a prompt they did not ask for — as it could
|
|
// draw a window of its own. The prompt says who asks and for what, so the operator types only into a
|
|
// prompt they started.
|
|
|
|
// deskPromptWithin is how long the prompt waits for the operator: below the runtime's thirty seconds for
|
|
// one call, as the launcher's menu is.
|
|
const deskPromptWithin = 25
|
|
|
|
// deskGive is the desk path, its four reaches given so a test needs no store and no bus.
|
|
type deskGive struct {
|
|
// declares refuses a module or a secret the mesh would refuse, before anybody is asked to type.
|
|
declares func(module, name string) error
|
|
// ask asks one machine's node-launcher.secret and answers its result, or the holder's refusal.
|
|
ask func(machine string, args map[string]any) (json.RawMessage, error)
|
|
// accept seals the value as `secret accept` does, and says whether it lives until the module's start.
|
|
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.
|
|
var errNothingGiven = errors.New("nothing was given")
|
|
|
|
// give asks the desk for the value and seals it; it answers the words said to the caller.
|
|
func (d deskGive) give(node, module, name, desk string) (string, error) {
|
|
for what, v := range map[string]string{"the machine": node, "the module": module, "the secret": name, "the desk": desk} {
|
|
if strings.TrimSpace(v) == "" {
|
|
return "", fmt.Errorf("%s is not named", what)
|
|
}
|
|
}
|
|
if err := d.declares(module, name); err != nil {
|
|
return "", fmt.Errorf("nobody was asked to type anything: %w", err)
|
|
}
|
|
public, private, err := secrets.Keypair()
|
|
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{
|
|
"module": module,
|
|
"secret": name,
|
|
"node": node,
|
|
"seal_to": public,
|
|
"timeout_seconds": deskPromptWithin,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("the desk on %s could not be asked: %w", desk, err)
|
|
}
|
|
var answer struct {
|
|
Sealed string `json:"sealed"`
|
|
Cancelled bool `json:"cancelled"`
|
|
TimedOut bool `json:"timed_out"`
|
|
}
|
|
if err := json.Unmarshal(raw, &answer); err != nil {
|
|
return "", fmt.Errorf("the desk on %s answered something that is not the prompt's answer", desk)
|
|
}
|
|
switch {
|
|
case answer.TimedOut:
|
|
return "", fmt.Errorf("%w: the prompt on %s was not answered within %d seconds", errNothingGiven, desk, deskPromptWithin)
|
|
case answer.Cancelled:
|
|
return "", fmt.Errorf("%w: the prompt on %s was dismissed", errNothingGiven, desk)
|
|
case answer.Sealed == "":
|
|
return "", fmt.Errorf("the desk on %s answered no sealed value", desk)
|
|
}
|
|
opened, err := secrets.Open(private, answer.Sealed)
|
|
if err != nil {
|
|
// Never the value, never what failed to open: only that it was not sealed to this call.
|
|
return "", fmt.Errorf("the desk on %s answered a value not sealed to this call; nothing was taken", desk)
|
|
}
|
|
value := asSupplied(string(opened))
|
|
for i := range opened {
|
|
opened[i] = 0
|
|
}
|
|
if strings.TrimSpace(value) == "" {
|
|
return "", fmt.Errorf("%w: the prompt on %s was answered empty", errNothingGiven, desk)
|
|
}
|
|
untilStart, err := d.accept(value)
|
|
value = ""
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
act := link.HandAct{Verb: "secret accept", Args: []string{node, module, name, "--at-desk", desk},
|
|
Why: fmt.Sprintf("the operator gave %s for %s on %s at the desk on %s", name, module, node, desk),
|
|
Cause: "given-at-the-desk"}
|
|
recorded := ""
|
|
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 {
|
|
words += fmt.Sprintf("\n it lives until %s next starts well under the mesh, and is then replaced with a value "+
|
|
"the mesh makes (ADR 0228)", module)
|
|
}
|
|
return words + recorded, nil
|
|
}
|
|
|
|
// giveAtDesk is `secret accept <node> <module> <name> --at-desk <machine>`: the desk path, on this
|
|
// controller's stores and bus.
|
|
func giveAtDesk(ctx context.Context, node, module, name, desk string) error {
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
d := deskGive{
|
|
declares: func(module, name string) error { return open.inventory.DeclaresOwnSecret(ctx, module, name) },
|
|
ask: func(machine string, args map[string]any) (json.RawMessage, error) {
|
|
var result json.RawMessage
|
|
err := onTheBus(func(conn *nats.Conn) error {
|
|
answer, err := link.AskSeatTool(ctx, conn, "node-launcher", "secret", machine, args,
|
|
time.Duration(deskPromptWithin+5)*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if answer.Error != "" {
|
|
return errors.New(answer.Error)
|
|
}
|
|
result = answer.Result
|
|
return nil
|
|
})
|
|
return result, err
|
|
},
|
|
accept: func(value string) (bool, error) {
|
|
return open.inventory.AcceptGivenSecret(ctx, node, module, name, value)
|
|
},
|
|
record: func(act link.HandAct) error {
|
|
return onTheBus(func(conn *nats.Conn) error {
|
|
_, err := link.RecordHandAct(ctx, conn, act)
|
|
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 {
|
|
return err
|
|
}
|
|
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
|
|
})
|
|
}
|