nox records the hand act a sensitive vault verb is warranted by
Each sensitive vault verb judges its own caller, and nox reaches the vault as its machine's runtime, never as a person, so it could use none of them. Before rotate, retire approve, cleanup delete and secret export nox records the act through mesh-hand-acts (the vault verb as its verb, what it acts on as its what) and names it as act; a refused recording calls the vault not at all (ADR 0590 §8, issue 1126 review 1). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -61,12 +61,33 @@ func (u usageError) Error() string { return u.msg }
|
||||
|
||||
func usageErr(format string, a ...any) error { return usageError{fmt.Sprintf(format, a...)} }
|
||||
|
||||
// call is what one line asks: an address and its arguments.
|
||||
// call is what one line asks: an address and its arguments. For a sensitive vault verb, its argument `act` holds the
|
||||
// *handAct that warrants it until run records the act and puts its id there.
|
||||
type call struct {
|
||||
Address string
|
||||
Args map[string]any
|
||||
}
|
||||
|
||||
// handAct is the act nox records through mesh-hand-acts before a sensitive vault verb, and names to it as `act`: each such
|
||||
// verb judges its own caller, and nox reaches the vault as its machine's runtime, never as a person (novox/hq ADR 0590
|
||||
// §8). Its verb is the vault verb's address; its what names what the verb acts on, several names space-separated.
|
||||
type handAct struct {
|
||||
Verb, What, Why string
|
||||
}
|
||||
|
||||
// warranted is c with the hand act that warrants it, for the vault verb at its address.
|
||||
func warranted(c call, what string) call {
|
||||
why, _ := c.Args["why"].(string)
|
||||
if strings.TrimSpace(why) == "" {
|
||||
why = "asked at the operator's terminal"
|
||||
}
|
||||
if c.Args == nil {
|
||||
c.Args = map[string]any{}
|
||||
}
|
||||
c.Args["act"] = &handAct{Verb: c.Address, What: what, Why: why}
|
||||
return c
|
||||
}
|
||||
|
||||
// command is one of nox's lines.
|
||||
type command struct {
|
||||
words []string
|
||||
@@ -287,7 +308,10 @@ var commands = []command{
|
||||
return call{"mesh-config.config", a}, err
|
||||
}},
|
||||
{words: []string{"secret", "export"}, does: "every secret the vault holds, as its copy sealed to the operator key",
|
||||
build: fixed("mesh-vault.secret_vault_export")},
|
||||
build: func(l line) (call, error) {
|
||||
c, err := fixed("mesh-vault.secret_vault_export")(l)
|
||||
return warranted(c, "the vault's sealed copies"), err
|
||||
}},
|
||||
{words: []string{"secret", "accept"}, instead: "mesh-vault.accept takes a value from the controller alone, and no seat takes one from a person yet; a module's own secret is asked at a desk (mesh-vault.secret-ask)"},
|
||||
{words: []string{"secret", "recover"}, instead: "opening a sealed copy locally is not built yet (beside nox enrol open); nox secret export gives the sealed copies"},
|
||||
{words: []string{"secret"}, instead: "nox secret export; mesh-vault.rotate makes one anew"},
|
||||
@@ -304,11 +328,18 @@ var commands = []command{
|
||||
if !l.has("why") {
|
||||
return call{}, usageErr("give --why")
|
||||
}
|
||||
return call{"mesh-vault.rotate", pick(l, "node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why")}, nil
|
||||
what := "provision/" + l.flags["provider"] + "/" + l.flags["provision"]
|
||||
if l.has("node") || l.has("module") || l.has("name") {
|
||||
what = "own/" + l.flags["node"] + "/" + l.flags["module"] + "/" + l.flags["name"]
|
||||
}
|
||||
return warranted(call{"mesh-vault.rotate", pick(l, "node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why")}, what), nil
|
||||
}},
|
||||
{words: []string{"identity", "show"}, does: "the vault's signing key and authority", build: fixed("mesh-vault.vault_signers")},
|
||||
{words: []string{"retire", "approve"}, usage: "<consumer>... --why <text>", does: "retire the set waiting for a person",
|
||||
allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_approve")},
|
||||
allowed: []string{"why"}, build: func(l line) (call, error) {
|
||||
c, err := retirement("mesh-vault.provisioner_retire_approve")(l)
|
||||
return warranted(c, strings.Join(l.pos, " ")), err
|
||||
}},
|
||||
{words: []string{"retire", "reject"}, usage: "<consumer>... --why <text>", does: "keep the set waiting for a person active",
|
||||
allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_reject")},
|
||||
{words: []string{"retire"}, does: "every retirement waiting for a person", build: fixed("mesh-vault.provisioner_retirement")},
|
||||
@@ -324,7 +355,7 @@ var commands = []command{
|
||||
if l.has("confirm") {
|
||||
a["confirm"] = l.pos[0]
|
||||
}
|
||||
return call{"mesh-vault.provisioner_delete", a}, nil
|
||||
return warranted(call{"mesh-vault.provisioner_delete", a}, l.pos[0]), nil
|
||||
}},
|
||||
{words: []string{"cleanup"}, does: "every retired holder per provider", build: fixed("mesh-vault.provisioner_retirement")},
|
||||
{words: []string{"artifacts"}, usage: "[--collected]", does: "every artifact the mesh made: kept and why, or eligible",
|
||||
|
||||
@@ -7,6 +7,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -89,12 +90,43 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
fmt.Fprintf(stderr, "mesh-cli: %v. Nothing was called\n nox %s %s\n", err, strings.Join(c.words, " "), c.usage)
|
||||
return 2
|
||||
}
|
||||
if h, ok := k.Args["act"].(*handAct); ok {
|
||||
id, code := recordAct(ctx, *h, stderr)
|
||||
if code != 0 {
|
||||
return code
|
||||
}
|
||||
k.Args["act"] = id
|
||||
}
|
||||
if k.Address == "mesh-delivery-view.view_status" {
|
||||
return status(ctx, stdout, stderr)
|
||||
}
|
||||
return show(ctx, "mesh_call", map[string]any{"address": k.Address, "arguments": k.Args}, c.render, stdout, stderr)
|
||||
}
|
||||
|
||||
// recordAct records the hand act a sensitive vault verb is warranted by, through mesh-hand-acts, and answers its id;
|
||||
// a recording refused or unread calls the vault not at all (novox/hq ADR 0590 §8).
|
||||
func recordAct(ctx context.Context, h handAct, stderr io.Writer) (string, int) {
|
||||
a, err := meshCall(ctx, "mesh-hand-acts.hand-act", map[string]any{"verb": h.Verb, "what": h.What, "why": h.Why,
|
||||
"cause": "operator"})
|
||||
if err != nil {
|
||||
return "", failed(err, stderr)
|
||||
}
|
||||
if a.IsError {
|
||||
fmt.Fprintln(stderr, "mesh-cli: the hand act was not recorded, so "+h.Verb+" was not called: "+strings.TrimRight(a.Text, "\n"))
|
||||
return "", 1
|
||||
}
|
||||
var rec struct {
|
||||
Act struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"act"`
|
||||
}
|
||||
if err := json.NewDecoder(strings.NewReader(a.Text)).Decode(&rec); err != nil || rec.Act.ID == "" {
|
||||
fmt.Fprintf(stderr, "mesh-cli: mesh-hand-acts answered no act's id (%q), so %s was not called\n", a.Text, h.Verb)
|
||||
return "", 1
|
||||
}
|
||||
return rec.Act.ID, 0
|
||||
}
|
||||
|
||||
// noOwner says a line has no owner since the controller went, and what to use.
|
||||
func noOwner(what, instead string, stderr io.Writer) int {
|
||||
fmt.Fprintf(stderr, "mesh-cli: nox %s: %s.\n", what, gone)
|
||||
|
||||
@@ -120,12 +120,7 @@ func TestEachLineCallsItsOwner(t *testing.T) {
|
||||
{"settings set zsh execute withhold --node g14 --why test", "mesh-config.config", `{"action":"set","key":"execute","module":"zsh","node":"g14","value":"withhold","why":"test"}`},
|
||||
{"settings clear zsh execute --node g14 --why test", "mesh-config.config", `{"action":"clear","key":"execute","module":"zsh","node":"g14","why":"test"}`},
|
||||
{"config history --key k --limit 5", "mesh-config.config", `{"action":"history","key":"k","limit":5}`},
|
||||
{"secret export", "mesh-vault.secret_vault_export", `{}`},
|
||||
{"operator key show", "mesh-vault.vault_status", `{}`},
|
||||
{"rotate --node ace --module plex --name token --why leak", "mesh-vault.rotate", `{"module":"plex","name":"token","node":"ace","why":"leak"}`},
|
||||
{"retire approve c1 c2 --why done", "mesh-vault.provisioner_retire_approve", `{"consumers":["c1","c2"],"why":"done"}`},
|
||||
{"cleanup delete ace.plex --why done --confirm", "mesh-vault.provisioner_delete", `{"confirm":"ace.plex","consumer":"ace.plex","why":"done"}`},
|
||||
{"cleanup delete ace.plex --why dry", "mesh-vault.provisioner_delete", `{"consumer":"ace.plex","why":"dry"}`},
|
||||
{"collect --confirm --why space", "mesh-artifact-sweep.collect", `{"confirm":"true","why":"space"}`},
|
||||
{"images ace", "mesh-artifact-sweep.images", `{"node":"ace"}`},
|
||||
{"licence use g14 work", "anthropic-licence-manager.switch", `{"consumer":"g14","licence":"work"}`},
|
||||
@@ -285,3 +280,40 @@ func TestEveryLineIsInTheHelpOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0590 §8 (issue 1126 review 1): each sensitive vault verb judges its own caller, and nox reaches the vault as its
|
||||
// machine's runtime, so nox first records the hand act that warrants the line through mesh-hand-acts (the vault verb as
|
||||
// its verb, what it acts on as its what) and names it as `act`; a recording refused calls the vault not at all.
|
||||
func TestASensitiveLineRecordsItsHandActAndNamesIt(t *testing.T) {
|
||||
recorded := `{"act":{"id":"act-7","verb":"mesh-vault.x"},"said":"recorded"}`
|
||||
for _, c := range []struct{ line, address, verb, what, args string }{
|
||||
{"secret export", "mesh-vault.secret_vault_export", "mesh-vault.secret_vault_export", "the vault's sealed copies", `{"act":"act-7"}`},
|
||||
{"rotate --node ace --module plex --name token --why leak", "mesh-vault.rotate", "mesh-vault.rotate", "own/ace/plex/token",
|
||||
`{"act":"act-7","module":"plex","name":"token","node":"ace","why":"leak"}`},
|
||||
{"rotate --provider novox --provision postgres --why yearly", "mesh-vault.rotate", "mesh-vault.rotate", "provision/novox/postgres",
|
||||
`{"act":"act-7","provider":"novox","provision":"postgres","why":"yearly"}`},
|
||||
{"retire approve c1 c2 --why done", "mesh-vault.provisioner_retire_approve", "mesh-vault.provisioner_retire_approve", "c1 c2",
|
||||
`{"act":"act-7","consumers":["c1","c2"],"why":"done"}`},
|
||||
{"cleanup delete ace.plex --why done --confirm", "mesh-vault.provisioner_delete", "mesh-vault.provisioner_delete", "ace.plex",
|
||||
`{"act":"act-7","confirm":"ace.plex","consumer":"ace.plex","why":"done"}`},
|
||||
{"cleanup delete ace.plex --why dry", "mesh-vault.provisioner_delete", "mesh-vault.provisioner_delete", "ace.plex",
|
||||
`{"act":"act-7","consumer":"ace.plex","why":"dry"}`},
|
||||
} {
|
||||
got := console(t, map[string]string{"mesh-hand-acts.hand-act": recorded})
|
||||
code, _, errOut := runLine(strings.Fields(c.line)...)
|
||||
if code != 0 || len(*got) != 2 || (*got)[0].Address != "mesh-hand-acts.hand-act" || (*got)[1].Address != c.address ||
|
||||
(*got)[1].Args != c.args {
|
||||
t.Errorf("nox %s: exit %d, asked %+v, stderr %q", c.line, code, *got, errOut)
|
||||
continue
|
||||
}
|
||||
var act map[string]any
|
||||
_ = json.Unmarshal([]byte((*got)[0].Args), &act)
|
||||
if act["verb"] != c.verb || act["what"] != c.what || act["why"] == "" || act["cause"] == "" {
|
||||
t.Errorf("nox %s recorded %v", c.line, act)
|
||||
}
|
||||
}
|
||||
got := console(t, map[string]string{"mesh-hand-acts.hand-act": "!nothing serves seat:mesh-hand-acts.hand-act"})
|
||||
if code, _, errOut := runLine("secret", "export"); code != 1 || len(*got) != 1 || !strings.Contains(errOut, "nothing serves") {
|
||||
t.Errorf("a refused recording: exit %d, asked %+v, %q", code, *got, errOut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +70,26 @@ func TestTheSensitiveToolsRefuseACallerTheyDoNotJudgeTheirs(t *testing.T) {
|
||||
t.Errorf("a listing without sealed copies was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Review 1 of issue 1126: nox reaches the vault as its machine's runtime (`<node>.node-tools`), never as a person; it
|
||||
// records the hand act first and names it, and so the export answers it, and refuses it without.
|
||||
func TestNoxsRuntimeExportsWithItsHandActAndIsRefusedWithout(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.ReadAct = func(id string) (*HandActSeen, error) {
|
||||
if id != "nox-1" {
|
||||
return nil, nil
|
||||
}
|
||||
return &HandActSeen{ID: id, By: "shanks.node-tools", At: v.Now(), Verb: "mesh-vault." + SensitiveVaultExport,
|
||||
Args: []string{"the vault's sealed copies"}}, nil
|
||||
}
|
||||
export := toolNamed(t, definitions(&Env{Vault: v.Vault}), SensitiveVaultExport)
|
||||
if _, err := export.runAs("shanks.node-tools", map[string]any{}); err == nil || !strings.Contains(err.Error(), "named none") {
|
||||
t.Errorf("an export with no act: %v", err)
|
||||
}
|
||||
if _, err := export.runAs("shanks.node-tools", map[string]any{"act": "nox-1"}); err != nil {
|
||||
t.Errorf("an export with nox's act: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user