Stop serving the vault's move-asks, which only the controller called
move-asks copied the controller's record of asks into the vault's once; the controller is not placed (ADR 0590) and nothing else calls it. move-in stays: the node-engine calls it. The drift test names the node's own composition now that the controller composes for none (issue 1126). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -33,7 +33,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -259,96 +258,6 @@ func (v *Vault) MoveTake(caller, call, sealed, readBackTo string) (MovedIn, erro
|
||||
|
||||
// ---- move-asks ----------------------------------------------------------------------------------------------
|
||||
|
||||
// MovedAsk is one row of the controller's record of asks at a desk (its secret_ask table): no value.
|
||||
type MovedAsk struct {
|
||||
ID int64 `json:"id"`
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
Name string `json:"name"`
|
||||
Desk string `json:"desk"`
|
||||
AskedBy string `json:"asked_by"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||
Outcome string `json:"outcome,omitempty"`
|
||||
}
|
||||
|
||||
// AsksMoved is what a copy of the record of asks did.
|
||||
type AsksMoved struct {
|
||||
Copied int `json:"copied"`
|
||||
Already int `json:"already"`
|
||||
}
|
||||
|
||||
// MovedAsksAtOnce bounds one call: the controller sends its record in parts.
|
||||
const MovedAsksAtOnce = 500
|
||||
|
||||
// movedAskID is the vault's id of a row of the controller's record.
|
||||
func movedAskID(id int64) string { return "controller-" + strconv.FormatInt(id, 10) }
|
||||
|
||||
// MoveAsks copies rows of the controller's record of asks into the vault's, as they are: each ask's opening and,
|
||||
// when it ended, its end — once, so a copy run again adds nothing. From the controller alone, whose record it is.
|
||||
// Every row is judged before any is written.
|
||||
func (v *Vault) MoveAsks(caller string, asks []MovedAsk) (AsksMoved, error) {
|
||||
if err := fromTheController(caller, "move-asks"); err != nil {
|
||||
return AsksMoved{}, err
|
||||
}
|
||||
if len(asks) > MovedAsksAtOnce {
|
||||
return AsksMoved{}, fmt.Errorf("%d asks at once is more than %d; send them in parts", len(asks), MovedAsksAtOnce)
|
||||
}
|
||||
type lines struct{ opened, ended AskRecord }
|
||||
var all []lines
|
||||
for _, a := range asks {
|
||||
secret, err := OwnSecret(a.Node, a.Module, a.Name)
|
||||
if err != nil {
|
||||
return AsksMoved{}, fmt.Errorf("ask %d: %w; nothing was copied", a.ID, err)
|
||||
}
|
||||
if !namePart.MatchString(a.Desk) || a.OpenedAt.IsZero() || a.ID <= 0 {
|
||||
return AsksMoved{}, fmt.Errorf("ask %d names no desk, no time it opened or no id; nothing was copied", a.ID)
|
||||
}
|
||||
l := lines{opened: AskRecord{ID: movedAskID(a.ID), At: a.OpenedAt.UTC(), Event: AskOpened, Secret: secret,
|
||||
Node: a.Node, Module: a.Module, Name: a.Name, Desk: a.Desk, AskedBy: askedByName(a.AskedBy),
|
||||
Caller: ControllerCaller}}
|
||||
if a.EndedAt != nil {
|
||||
if a.Outcome != "given" && !askOutcomes[a.Outcome] {
|
||||
return AsksMoved{}, fmt.Errorf("ask %d ended %q, which is no outcome the record knows; nothing was "+
|
||||
"copied", a.ID, a.Outcome)
|
||||
}
|
||||
l.ended = AskRecord{ID: movedAskID(a.ID), At: a.EndedAt.UTC(), Event: AskEnded, Secret: secret,
|
||||
Outcome: a.Outcome}
|
||||
}
|
||||
all = append(all, l)
|
||||
}
|
||||
v.Store.asksMu.Lock()
|
||||
defer v.Store.asksMu.Unlock()
|
||||
held, err := v.Store.asks()
|
||||
if err != nil {
|
||||
return AsksMoved{}, fmt.Errorf("the record of asks cannot be read: %w; nothing was copied", err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, r := range held {
|
||||
if r.Event == AskOpened {
|
||||
seen[r.ID] = true
|
||||
}
|
||||
}
|
||||
out := AsksMoved{}
|
||||
for _, l := range all {
|
||||
if seen[l.opened.ID] {
|
||||
out.Already++
|
||||
continue
|
||||
}
|
||||
if err := v.recordAsk(l.opened); err != nil {
|
||||
return out, fmt.Errorf("ask %s could not be copied, after %d: %w", l.opened.ID, out.Copied, err)
|
||||
}
|
||||
if l.ended.ID != "" {
|
||||
if err := v.recordAsk(l.ended); err != nil {
|
||||
return out, fmt.Errorf("the end of ask %s could not be copied, after %d: %w", l.opened.ID, out.Copied, err)
|
||||
}
|
||||
}
|
||||
seen[l.opened.ID] = true
|
||||
out.Copied++
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// StateNodeKey records the sealing key the node-engine of caller's machine states in its own name (novox/hq ADR 0372,
|
||||
// issue 786), at its start and whenever the key changes, whether or not it ever moved a secret in. The machine is the
|
||||
// caller's, as the bus proved it (`node.<machine>`), never an argument. A first key is handled like a changed one: the
|
||||
|
||||
@@ -239,57 +239,6 @@ func TestAMoveTakesNothingItCannotOpenOrReadBack(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The controller's record of asks is copied as it is, once: each ask's opening and its end, who asked and at which
|
||||
// desk, and the bounds then count them. From the controller alone.
|
||||
func TestTheRecordOfAsksIsCopiedAsItIsOnce(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
now := v.Now()
|
||||
ended := now.Add(-30 * time.Second)
|
||||
asks := []MovedAsk{
|
||||
{ID: 41, Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: "g14/claude-code",
|
||||
OpenedAt: now.Add(-50 * time.Minute), EndedAt: &ended, Outcome: "given"},
|
||||
{ID: 42, Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: "g14/claude-code",
|
||||
OpenedAt: now.Add(-40 * time.Minute), EndedAt: &ended, Outcome: "dismissed"},
|
||||
{ID: 43, Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: "ace/agent",
|
||||
OpenedAt: now.Add(-20 * time.Minute)},
|
||||
}
|
||||
if _, err := v.MoveAsks("node.anchor", asks); err == nil {
|
||||
t.Error("a node-engine copied the record of asks")
|
||||
}
|
||||
copied, err := v.MoveAsks(ControllerCaller, asks)
|
||||
if err != nil || copied.Copied != 3 || copied.Already != 0 {
|
||||
t.Fatalf("copied %+v, %v", copied, err)
|
||||
}
|
||||
again, err := v.MoveAsks(ControllerCaller, asks)
|
||||
if err != nil || again.Copied != 0 || again.Already != 3 {
|
||||
t.Fatalf("copied again %+v, %v", again, err)
|
||||
}
|
||||
all, _ := v.Store.Asks(100)
|
||||
if len(all) != 5 {
|
||||
t.Fatalf("%d lines: %+v", len(all), all)
|
||||
}
|
||||
opened := map[string]AskRecord{}
|
||||
for _, r := range all {
|
||||
if r.Event == AskOpened {
|
||||
opened[r.ID] = r
|
||||
}
|
||||
}
|
||||
if r := opened["controller-41"]; r.Secret != "own/anchor/telegram/telegram-token" || r.AskedBy != "g14/claude-code" ||
|
||||
r.Desk != "laptop" || !r.At.Equal(asks[0].OpenedAt) || r.Caller != ControllerCaller {
|
||||
t.Errorf("ask 41: %+v", r)
|
||||
}
|
||||
// Three asks in the hour, from the controller's record: the vault's bound holds them.
|
||||
if _, err := v.AskOpen(ControllerCaller, telegramAsk("g14/claude-code")); err == nil ||
|
||||
!strings.Contains(err.Error(), "in the last hour") {
|
||||
t.Errorf("an ask after three copied in the hour: %v", err)
|
||||
}
|
||||
bad := []MovedAsk{{ID: 44, Node: "anchor", Module: "telegram", Name: "x", Desk: "laptop", AskedBy: "a",
|
||||
OpenedAt: now, EndedAt: &ended, Outcome: "whatever"}}
|
||||
if _, err := v.MoveAsks(ControllerCaller, bad); err == nil {
|
||||
t.Error("an ask that ended in no outcome the record knows was copied")
|
||||
}
|
||||
}
|
||||
|
||||
// The two steps as the runtime calls the tool: the caller is the one it proved, never an argument.
|
||||
func TestMoveInThroughTheTool(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
@@ -317,13 +266,6 @@ func TestMoveInThroughTheTool(t *testing.T) {
|
||||
if rec.MadeAt.Year() != 2025 || rec.MovedFrom == nil || rec.MovedFrom.Generation != "g-1" {
|
||||
t.Errorf("kept %+v", rec)
|
||||
}
|
||||
asks := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".move-asks")
|
||||
moved, err := asks.runAs("controller", map[string]any{"asks": []any{map[string]any{"id": 7, "node": "anchor",
|
||||
"module": "telegram", "name": "telegram-token", "desk": "laptop", "asked_by": "g14/claude-code",
|
||||
"opened_at": "2026-10-01T10:00:00Z", "ended_at": "2026-10-01T10:00:20Z", "outcome": "given"}}})
|
||||
if err != nil || moved.(AsksMoved).Copied != 1 {
|
||||
t.Fatalf("asks through the tool: %+v, %v", moved, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1126, cause C4: a machine's node-engine hands the vault what runs of its own machine's own secret, where the
|
||||
@@ -445,7 +387,7 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) {
|
||||
Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "postgres.needs-superuser-password",
|
||||
Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}}
|
||||
have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"pro": answer},
|
||||
Composes: delivery.ComposesController})
|
||||
Composes: delivery.ComposesOwn})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -457,7 +399,7 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) {
|
||||
}
|
||||
if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" ||
|
||||
drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable ||
|
||||
len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["pro"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController {
|
||||
len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["pro"] != 1 || drift.Nodes[0].Composes != delivery.ComposesOwn {
|
||||
t.Errorf("own_drift: %+v", drift)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,28 +350,6 @@ func definitions(env *Env) []toolDef {
|
||||
}
|
||||
return v.OwnOfNode(caller, asked...)
|
||||
}},
|
||||
{Name: Seat + ".move-asks",
|
||||
Description: "Copy rows of the controller's record of asks at a desk into the vault's, as they are and once " +
|
||||
"(design 57's V3): from the controller alone, at most 500 at a time. No value is in any of them.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"asks": map[string]any{"type": "array", "description": "the controller's rows: id, node, module, name, " +
|
||||
"desk, asked_by, opened_at, and ended_at and outcome when the ask ended",
|
||||
"items": map[string]any{"type": "object"}}}},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
v, err := env.ready()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := json.Marshal(a["asks"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var asks []MovedAsk
|
||||
if err := json.Unmarshal(raw, &asks); err != nil {
|
||||
return nil, fmt.Errorf("asks is not the controller's rows: %w", err)
|
||||
}
|
||||
return v.MoveAsks(caller, asks)
|
||||
}},
|
||||
{Name: "secret_asks",
|
||||
Description: "The record of asks for a module's own secret at a desk, newest first: who asked as the bus named " +
|
||||
"them, for what, at which desk, when, and how each ended. Never a value.",
|
||||
|
||||
@@ -204,7 +204,6 @@
|
||||
"rotate",
|
||||
"move-in",
|
||||
"node-key",
|
||||
"move-asks",
|
||||
"own",
|
||||
"issue-account",
|
||||
"sign",
|
||||
|
||||
Reference in New Issue
Block a user