The give verb opened the desk's hidden prompt for a module's own secret, with the desk named. Now the secret-ask verb (secret ask <node> <module> <name> [--at <desk>]) opens the same prompt from anywhere on the mesh, with the desk the module's machine unless named, and give composes the same line. Every ask is recorded in the store before the prompt opens (migration 0091): one open ask per secret, three an hour, so an agent cannot keep a prompt in front of the operator. The prompt names who asked, from the bus's word on the caller cut to a name's characters, never an argument of the call. The value stays typed at the desk, sealed to the one call and then to the module's machine, never in an argument, a log or an event; a secret the mesh makes itself and a trusted party's secret are refused as before.
334 lines
15 KiB
Go
334 lines
15 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, ADR 0277).
|
|
//
|
|
// mesh-controller secret ask <node> <module> <name> [--at <desk>]
|
|
//
|
|
// **The value never passes through whoever asked for it.** An agent, or the operator at the mesh MCP
|
|
// server, calls `secret-ask` (or `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.
|
|
//
|
|
// **Bounded, and the prompt says who asked** (ADR 0277): one open prompt per secret and few an hour, read from
|
|
// the store before the prompt opens (inventory.OpenSecretAsk), so an agent cannot keep a prompt in front of the
|
|
// operator until they type. The prompt names the module, the secret, the machine and who asked — the caller as
|
|
// the bus named it, never a word the caller chose — written by the desk's launcher from those names alone.
|
|
//
|
|
// **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
|
|
// known refuses a machine the mesh does not know, before anybody is asked to type; nil knows every one
|
|
// (a test that does not look).
|
|
known func(machine string) error
|
|
// trusted says a module runs as an account of its own: its secret is never taken at a desk (below). Nil is
|
|
// never (a test that does not look).
|
|
trusted func(module string) (bool, 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
|
|
// askedBy is who asked, as the bus named the caller: said in the prompt and recorded.
|
|
askedBy string
|
|
// open records the ask and holds the bounds (one open per secret, few an hour), answering the record's id;
|
|
// nil keeps no record (a test that does not look). end closes it with how it ended.
|
|
open func(node, module, name, desk string) (int64, error)
|
|
end func(id int64, outcome string) error
|
|
}
|
|
|
|
// askedByName is the caller as the prompt names it: the first clause of what the bus said, in the characters
|
|
// a name has, at most 80 of them. The desk's launcher refuses anything else, so no words of the caller's own
|
|
// reach the prompt.
|
|
func askedByName(caller string) string {
|
|
first, _, _ := strings.Cut(caller, ",")
|
|
var b strings.Builder
|
|
for _, r := range strings.TrimSpace(first) {
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '_', r == '/', r == '@',
|
|
r == '-', r == ' ':
|
|
b.WriteRune(r)
|
|
default:
|
|
b.WriteRune('-')
|
|
}
|
|
if b.Len() >= 80 {
|
|
break
|
|
}
|
|
}
|
|
name := strings.TrimSpace(b.String())
|
|
if name == "" || strings.HasPrefix(name, "-") {
|
|
return "an unnamed caller"
|
|
}
|
|
return name
|
|
}
|
|
|
|
// 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 d.known != nil {
|
|
for what, machine := range map[string]string{"the machine the secret is for": node, "the desk": desk} {
|
|
if err := d.known(machine); err != nil {
|
|
return "", fmt.Errorf("nobody was asked to type anything: %s, %s, is not a machine this mesh knows: %w",
|
|
what, machine, err)
|
|
}
|
|
}
|
|
}
|
|
if err := d.declares(module, name); err != nil {
|
|
return "", fmt.Errorf("nobody was asked to type anything: %w", err)
|
|
}
|
|
// **A trusted party's secret is not taken at a desk** (the confirmation review of 2026-10-09, N1-give). The
|
|
// prompt's answer comes back over the desk machine's bus, on a subject that machine's runtime answers — and
|
|
// on a desk machine agents run as the operator, who holds that runtime's credential. An agent could answer
|
|
// first, with a bot token of its own sealed to this call's key, and the channel the operator's answers are
|
|
// proven on would be the agent's. So the value of a module running as its own account is typed at the
|
|
// controller's terminal, where no bus carries it.
|
|
if d.trusted != nil {
|
|
trusted, err := d.trusted(module)
|
|
if err != nil {
|
|
return "", fmt.Errorf("nobody was asked to type anything: whether %s is a trusted party could not be read: %w", module, err)
|
|
}
|
|
if trusted {
|
|
return "", fmt.Errorf("nobody was asked to type anything: %s runs as an account of its own and proves the "+
|
|
"operator's answers, so its %s is given at the controller's terminal alone — there, run `mesh-controller "+
|
|
"secret accept %s %s %s` and type it at its prompt. A desk's prompt is answered over the desk machine's "+
|
|
"bus, where an agent may answer first (novox/hq ADR 0259 §10)", module, name, node, module, name)
|
|
}
|
|
}
|
|
// The bounds, read and kept before anybody is asked to type (novox/hq ADR 0277): one open prompt per secret,
|
|
// few an hour. How the ask ends is recorded whatever happens below.
|
|
outcome := "failed"
|
|
if d.open != nil {
|
|
id, err := d.open(node, module, name, desk)
|
|
if err != nil {
|
|
return "", fmt.Errorf("nobody was asked to type anything: %w", err)
|
|
}
|
|
defer func() {
|
|
if d.end != nil {
|
|
_ = d.end(id, outcome)
|
|
}
|
|
}()
|
|
}
|
|
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). Who asked is the bus's word on the caller, cut to a
|
|
// name's characters — never an argument of the call.
|
|
raw, err := d.ask(desk, map[string]any{
|
|
"module": module,
|
|
"secret": name,
|
|
"node": node,
|
|
"asked_by": askedByName(d.askedBy),
|
|
"seal_to": public,
|
|
"timeout_seconds": deskPromptWithin,
|
|
})
|
|
if err != nil {
|
|
outcome = "refused"
|
|
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:
|
|
outcome = "timed-out"
|
|
return "", fmt.Errorf("%w: the prompt on %s was not answered within %d seconds", errNothingGiven, desk, deskPromptWithin)
|
|
case answer.Cancelled:
|
|
outcome = "dismissed"
|
|
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) == "" {
|
|
outcome = "empty"
|
|
return "", fmt.Errorf("%w: the prompt on %s was answered empty", errNothingGiven, desk)
|
|
}
|
|
untilStart, err := d.accept(value)
|
|
value = ""
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
outcome = "given"
|
|
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, asked by %s", name, module, node, desk,
|
|
askedByName(d.askedBy)),
|
|
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
|
|
}
|
|
|
|
// askAtDesk is `secret ask <node> <module> <name> [--at <machine>]`, and the terminal's `secret accept … --at-desk
|
|
// <machine>`: the desk path, on this controller's stores and bus. The desk is the module's machine unless named.
|
|
func askAtDesk(ctx context.Context, node, module, name, desk string) error {
|
|
if desk == "" {
|
|
desk = node
|
|
}
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
d := deskGive{
|
|
askedBy: link.Caller(),
|
|
open: func(node, module, name, desk string) (int64, error) {
|
|
return open.inventory.OpenSecretAsk(ctx, node, module, name, askedByName(link.Caller()), desk)
|
|
},
|
|
end: func(id int64, outcome string) error { return open.inventory.EndSecretAsk(ctx, id, outcome) },
|
|
declares: func(module, name string) error { return open.inventory.DeclaresOwnSecret(ctx, module, name) },
|
|
known: func(machine string) error {
|
|
_, err := open.inventory.NodeByName(ctx, machine)
|
|
return err
|
|
},
|
|
trusted: func(module string) (bool, error) { return open.inventory.RunsAsItsOwnAccount(ctx, module) },
|
|
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.
|
|
// The summary, for whoever looks closer, names the secret and the time. The words the operator reads are
|
|
// held to the plain rule (conditions.PlainWords): no clock time — the channel says when, in the operator's
|
|
// time — and the secret's name said as words. Words that broke the rule were replaced by the keeper with
|
|
// "needs a look … a problem it calls secret given" (hq issue 359), which told the operator nothing.
|
|
func secretGivenObservation(node, module, name, how string, at time.Time) conditions.Observation {
|
|
key := node + "." + module + "." + name
|
|
where := module + " on " + node
|
|
// Within the bounds whatever the names' length: the module and machine, else the module, else the machine.
|
|
headline := "New secret given for " + where
|
|
for _, h := range []string{"New secret given for " + module, "New secret given on " + node} {
|
|
if len(headline) > conditions.HeadlineMax {
|
|
headline = h
|
|
}
|
|
}
|
|
resolved := "You saw that " + module + " was given a new secret"
|
|
if len(resolved) > conditions.HeadlineMax+20 {
|
|
resolved = "You saw that a new secret was given on " + node
|
|
}
|
|
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: headline,
|
|
Explanation: fmt.Sprintf("The secret %s of %s was given %s. If you gave it, nothing else is needed. If you "+
|
|
"did not, somebody else now holds what %s acts with.", secretNameWords(name), where, how, module),
|
|
Needs: "silence this if you just gave it; if you did not, give it again yourself so that only you hold it.",
|
|
Resolved: resolved,
|
|
Actions: []conditions.Action{conditions.SilenceAction(conditions.Key(conditions.ScopeMachine, key, kindSecretGiven))}}
|
|
}
|
|
|
|
// secretNameWords is a secret's name as the operator reads it: "telegram-token" is "telegram token".
|
|
func secretNameWords(name string) string {
|
|
return strings.Join(strings.FieldsFunc(name, func(r rune) bool { return r == '-' || r == '_' || r == '.' }), " ")
|
|
}
|
|
|
|
// 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
|
|
})
|
|
}
|