A secret given tells the operator once, opens no condition (issue 835)
ADR 0380: giving a module's own secret now asks the operator channel's tell over the bus, naming the secret, where, when and who asked, and raises no secret-given condition. Until nothing serves tell yet, or the controller's grant is refused (link.ErrNothingServes / the new link.ErrGrantRefused), the condition is raised exactly as before, so the change composes whichever side of issue 797 merges first. A successful tell also retires every open secret-given condition. The terminal's own secret accept now records a hand act too, as the desk path already did. The controller's bus grant for operator-channel.tell is added to the composed config (golden fixture updated).
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -253,8 +254,9 @@ func askAtDesk(ctx context.Context, node, module, name, desk string) error {
|
||||
return err
|
||||
}
|
||||
defer open.Close()
|
||||
askedBy := link.Caller()
|
||||
d := deskGive{
|
||||
askedBy: link.Caller(),
|
||||
askedBy: askedBy,
|
||||
open: func(node, module, name, desk string) (int64, error) {
|
||||
return open.inventory.OpenSecretAsk(ctx, node, module, name, askedByName(link.Caller()), desk)
|
||||
},
|
||||
@@ -291,7 +293,9 @@ func askAtDesk(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) },
|
||||
announce: func(node, module, name, how string) error {
|
||||
return announceSecretGiven(ctx, node, module, name, how, askedBy)
|
||||
},
|
||||
}
|
||||
words, err := d.give(node, module, name, desk)
|
||||
if err != nil {
|
||||
@@ -342,10 +346,84 @@ 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
|
||||
// tellWithin is how long the operator channel's `tell` is given to answer: well under a human's patience,
|
||||
// and short because nothing in the call waits on the operator — it asks nothing (novox/hq ADR 0380).
|
||||
const tellWithin = 10 * time.Second
|
||||
|
||||
// askOperatorChannel asks the operator channel's seat (the holder issue 797 gives it) its `tell` verb,
|
||||
// through the controller's grant (novox/hq ADR 0380 §8), over this controller's own bus connection; a seam
|
||||
// a test replaces whole. The holder's own refusal is an error naming it; an absent holder or a refused
|
||||
// grant answer as link.ErrNothingServes or link.ErrGrantRefused (checked with errors.Is), so the caller can
|
||||
// tell the two apart from any other failure.
|
||||
var askOperatorChannel = func(ctx context.Context, args map[string]any) (json.RawMessage, error) {
|
||||
var result json.RawMessage
|
||||
err := onTheBus(func(conn *nats.Conn) error {
|
||||
answer, err := link.AskMeshSeatTool(ctx, conn, "operator-channel", "tell", args, tellWithin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if answer.Error != "" {
|
||||
return fmt.Errorf("operator-channel.tell refused: %s", answer.Error)
|
||||
}
|
||||
result = answer.Result
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// announceSecretGiven tells the operator, once, that a module's own secret was given (novox/hq ADR 0380):
|
||||
// one plain message through the operator channel's `tell`, naming the secret, the module, the machine,
|
||||
// where and when it was given and who asked — asking nothing, and opening no condition. **The controller's
|
||||
// change waits for that verb at run time, not at merge** (clause 9): while nothing on the bus yet serves it,
|
||||
// or the controller's own grant is refused, the secret-given condition is raised exactly as before ADR 0380
|
||||
// — any other failure (the holder itself refusing, or answering something unreadable) is the announcement's
|
||||
// own failure, said to the caller as it always was.
|
||||
func announceSecretGiven(ctx context.Context, node, module, name, how, askedBy string) error {
|
||||
at := time.Now()
|
||||
body := map[string]any{
|
||||
"headline": fmt.Sprintf("A password was entered for %s's %s", module, secretNameWords(name)),
|
||||
"lines": []string{
|
||||
fmt.Sprintf("%s at %s, asked by %s.", how, at.Local().Format("15:04"), askedByName(askedBy)),
|
||||
"If that was not you, give it again yourself at the controller's terminal.",
|
||||
},
|
||||
}
|
||||
_, told := askOperatorChannel(ctx, body)
|
||||
switch {
|
||||
case told == nil:
|
||||
clearSecretGivenConditions(ctx)
|
||||
return nil
|
||||
case errors.Is(told, link.ErrNothingServes), errors.Is(told, link.ErrGrantRefused):
|
||||
log.Printf("secret-given: %s's %s on %s could not be told through the operator channel (%v); the "+
|
||||
"secret-given condition is raised as it was before novox/hq ADR 0380 (its clause 9)", module, name,
|
||||
node, told)
|
||||
return withKeeper(ctx, func(k *conditions.Keeper) error {
|
||||
_, err := k.Observe(ctx, secretGivenObservation(node, module, name, how, at))
|
||||
return err
|
||||
})
|
||||
default:
|
||||
return told
|
||||
}
|
||||
}
|
||||
|
||||
// clearSecretGivenConditions retires every open secret-given condition once a plain message has replaced
|
||||
// it (novox/hq ADR 0380 §10): called after the first time a secret-given message is told successfully. No
|
||||
// resolved line is sent — the condition is simply gone — and why is kept in the keeper's own history.
|
||||
func clearSecretGivenConditions(ctx context.Context) {
|
||||
if err := withKeeper(ctx, func(k *conditions.Keeper) error {
|
||||
open, err := k.Open(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range open {
|
||||
if c.Kind != kindSecretGiven {
|
||||
continue
|
||||
}
|
||||
if _, err := k.ClearSaying(ctx, c.Key, "retired by novox/hq ADR 0380", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("secret-given: the open secret-given conditions, retired by ADR 0380, could not be cleared: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,3 +556,125 @@ func TestASecretAskIsNeverASecretRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// askOperatorChannelFake replaces askOperatorChannel for the test and records every call it was given.
|
||||
func askOperatorChannelFake(t *testing.T, answer func(args map[string]any) (json.RawMessage, error)) *[]map[string]any {
|
||||
t.Helper()
|
||||
var calls []map[string]any
|
||||
was := askOperatorChannel
|
||||
askOperatorChannel = func(_ context.Context, args map[string]any) (json.RawMessage, error) {
|
||||
calls = append(calls, args)
|
||||
return answer(args)
|
||||
}
|
||||
t.Cleanup(func() { askOperatorChannel = was })
|
||||
return &calls
|
||||
}
|
||||
|
||||
// announceWithRealTell wires a deskGive's announce to the real announceSecretGiven, as askAtDesk does,
|
||||
// rather than the fakes the tests above use to look at what `give` does around it.
|
||||
func announceWithRealTell(ctx context.Context, askedBy string) func(node, module, name, how string) error {
|
||||
return func(node, module, name, how string) error {
|
||||
return announceSecretGiven(ctx, node, module, name, how, askedBy)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0380 clauses 1-2: a secret given at the desk is told to the operator once, in a plain
|
||||
// message through the operator channel's `tell`, naming the secret, the module, where and when and who
|
||||
// asked, and opens no condition — the kind the review of 2026-10-09 (M4) raised is retired.
|
||||
func TestASecretGivenAtTheDeskIsToldOnceAndOpensNoCondition(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
k, _ := withConditionsInMemory(t)
|
||||
calls := askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, nil })
|
||||
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
||||
d.announce = announceWithRealTell(ctx, "mail-seats-835@novox")
|
||||
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(*calls) != 1 {
|
||||
t.Fatalf("told the operator %d times, want exactly 1: %v", len(*calls), *calls)
|
||||
}
|
||||
call := (*calls)[0]
|
||||
if call["headline"] != "A password was entered for telegram's telegram token" {
|
||||
t.Errorf("headline %v", call["headline"])
|
||||
}
|
||||
lines, _ := call["lines"].([]string)
|
||||
if len(lines) != 2 || !strings.Contains(lines[0], "at the desk on laptop at") ||
|
||||
!strings.Contains(lines[0], "asked by mail-seats-835@novox") ||
|
||||
lines[1] != "If that was not you, give it again yourself at the controller's terminal." {
|
||||
t.Errorf("lines %v", lines)
|
||||
}
|
||||
open, err := k.Open(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(open) != 0 {
|
||||
t.Errorf("a condition was raised though the operator was told: %+v", open)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0380 §9: until the operator channel's `tell` is delivered — nothing yet serves it, or the
|
||||
// controller's own grant is refused — the secret-given condition is raised exactly as before.
|
||||
func TestASecretGivenAtTheDeskRaisesTheConditionWhileNothingServesTell(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
for _, failure := range []error{link.ErrNothingServes, link.ErrGrantRefused} {
|
||||
k, _ := withConditionsInMemory(t)
|
||||
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, failure })
|
||||
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
||||
d.announce = announceWithRealTell(ctx, "mesh-mail-835, novox")
|
||||
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
open, err := k.Open(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(open) != 1 || open[0].Kind != kindSecretGiven {
|
||||
t.Fatalf("%v: the condition was not raised as before: %+v", failure, open)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0380 §10: once the operator channel is told successfully, every secret-given condition
|
||||
// still open from before this change is retired, without a resolved line of its own.
|
||||
func TestASuccessfulTellRetiresEveryOpenSecretGivenCondition(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
k, _ := withConditionsInMemory(t)
|
||||
for _, module := range []string{"telegram", "mesh-mail"} {
|
||||
if _, err := k.Observe(ctx, secretGivenObservation("anchor", module, "a-secret", "at the desk on laptop",
|
||||
time.Now())); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, nil })
|
||||
if err := announceSecretGiven(ctx, "anchor", "telegram", "telegram-token", "at the desk on laptop", "me"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
open, err := k.Open(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(open) != 0 {
|
||||
t.Fatalf("the open secret-given conditions were not retired: %+v", open)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0380 clause 7: a trusted party's secret given at the controller's terminal is announced
|
||||
// before it is kept, and not kept when the announcement fails. It still fails when telling the operator
|
||||
// AND raising the fallback condition both fail — nothing silently keeps a trusted party's value unheard.
|
||||
func TestATrustedPartysSecretAtTheTerminalIsNotKeptWhenTellAndTheConditionBothFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, link.ErrNothingServes })
|
||||
// conditionsFrom is left nil and there is no bus to dial here, so the fallback condition cannot be
|
||||
// raised either: both of ADR 0380's ways to tell the operator fail.
|
||||
kept := false
|
||||
if _, _, err := keepGiven(true,
|
||||
func() error {
|
||||
return announceSecretGiven(ctx, "anchor", "telegram", "telegram-token", "at the controller's terminal", "me")
|
||||
},
|
||||
func() (bool, error) { kept = true; return false, nil }); err == nil {
|
||||
t.Error("a trusted party's secret was kept though telling the operator and raising the condition both failed")
|
||||
}
|
||||
if kept {
|
||||
t.Error("the value was kept despite the failure")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
"github.com/novox/mesh-controller/internal/secrets"
|
||||
)
|
||||
|
||||
@@ -151,8 +154,11 @@ func secretCommand(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
askedBy := link.Caller()
|
||||
untilStart, unannounced, err := keepGiven(trusted,
|
||||
func() error { return announceSecretGiven(ctx, node, module, name, "at the controller's terminal") },
|
||||
func() error {
|
||||
return announceSecretGiven(ctx, node, module, name, "at the controller's terminal", askedBy)
|
||||
},
|
||||
func() (bool, error) { return open.inventory.AcceptGivenSecret(ctx, node, module, name, value) })
|
||||
if err != nil {
|
||||
if trusted && unannounced != nil {
|
||||
@@ -164,6 +170,17 @@ func secretCommand(ctx context.Context, args []string) error {
|
||||
if unannounced != nil {
|
||||
fmt.Printf(" this change could NOT be announced on the operator's channels: %v\n", unannounced)
|
||||
}
|
||||
// The giving is recorded, never the value (novox/hq ADR 0380 §6): the desk already logs its own act; the
|
||||
// terminal's own secret accept did not until now.
|
||||
act := link.HandAct{Verb: "secret accept", Args: []string{node, module, name},
|
||||
Why: fmt.Sprintf("the operator gave %s for %s on %s at the controller's terminal", name, module, node),
|
||||
Cause: "given-at-the-terminal"}
|
||||
if err := onTheBus(func(conn *nats.Conn) error {
|
||||
_, err := link.RecordHandAct(ctx, conn, act)
|
||||
return err
|
||||
}); err != nil {
|
||||
fmt.Printf(" this act could NOT be recorded in the hand-act log, and is done anyway: %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)
|
||||
|
||||
Reference in New Issue
Block a user