A trusted setting is proposed through the settings verb and set only on the operator's warrant (hq ADR 0277)
Issue 339 made every trusted setting the controller's terminal's alone, so an agent could only hand the operator a line to type at the control node. Now anyone the bus admits may PROPOSE a layer: settings propose keeps the proposal in the controller's own asks (the asked bucket, which the controller alone writes), judged as settings set judges, and asks the operator on the operator channel at the level approve with every key, its exact new value (in its shape where a path or an address may not leave the mesh), the was of a changed key, the removed keys and the layer's fingerprint. The serving controller sets the layer on the warrant alone: once, for the ask it holds, only when the record's values still digest to what the option bound and the layer is still the one shown, with the terminal's judgement and history, and keeps who approved it beside the layer, which settings says back (migration 0090). Decline, expiry, a cancel, a replacement or the router's refusal discard it; nothing is asked when no router, no grant or no channel can carry it. settings proposals lists them. The push afterwards is a separate act.
This commit is contained in:
@@ -268,17 +268,26 @@ var ControllerVerbs = []Verb{
|
||||
"with history, the layers it replaced. Setting replaces that layer whole and answers each key it adds (+), " +
|
||||
"changes (~) and removes (-); a set that would remove a key is refused unless replace says it is meant " +
|
||||
"(novox/hq ADR 0217). Takes effect at the next push. With clear, removes the layer and the module is back to " +
|
||||
"what its definition says; a cleared or replaced layer is kept in the history.",
|
||||
"what its definition says; a cleared or replaced layer is kept in the history. A trusted setting — " +
|
||||
"places, accesses, what a provider serves, what a trusted file asks for, the whole layer of a module " +
|
||||
"with a trusted mergeable file — is refused through this verb (novox/hq issue 339, 340) and PROPOSED " +
|
||||
"instead (novox/hq ADR 0277): with propose, the values (or clear) are put to the operator on a channel " +
|
||||
"that proves who answers, with every key and its exact new value, and the layer is set only on their " +
|
||||
"Approve; the answer names the proposal, its fingerprint and when it expires, and nothing changes until " +
|
||||
"then. With proposals, every proposal and where each stands; with proposal, one whole.",
|
||||
Input: schema(map[string]string{
|
||||
"module": "the module's name; with list, only that module's",
|
||||
"values": "the settings as a JSON object, for set",
|
||||
"values": "the settings as a JSON object, for set or propose",
|
||||
"node": "one machine; the whole mesh when absent",
|
||||
"clear": "\"true\" to remove the layer instead of setting it; not with values",
|
||||
"replace": "\"true\": with values, the set is meant to remove the keys the layer had and it does not name",
|
||||
"clear": "\"true\" to remove the layer instead of setting it, or to propose its removal; not with values",
|
||||
"replace": "\"true\": with values, the set (or the proposal) is meant to remove the keys the layer had and it does not name",
|
||||
"history": "\"true\": without values or clear, the layers this one replaced, the latest first",
|
||||
"list": "\"preferences\": every module's preferences — key, default and why — and the value on each " +
|
||||
"machine it is assigned to with where it comes from; module and node narrow it (novox/hq ADR 0262)",
|
||||
}, nil, "clear", "replace", "history")},
|
||||
"propose": "\"true\": propose the values (or clear) to the operator instead of setting them (novox/hq ADR 0277)",
|
||||
"proposals": "\"true\": every settings proposal, newest first, and where each stands",
|
||||
"proposal": "a proposal's id: that proposal whole, its values and the layer it was shown against",
|
||||
}, nil, "clear", "replace", "history", "propose", "proposals")},
|
||||
{Name: "command", Description: "Run one reading command line of the controller's own, as you would type it at " +
|
||||
"its shell — `node show ace`, `module list`, `plans`, `conditions show <key>` — and answer what it " +
|
||||
"printed. The generic verb beside the named ones (novox/hq ADR 0154), and since ADR 0266 it only reads: " +
|
||||
|
||||
@@ -799,7 +799,35 @@ func profileFrom(raw []byte) ([]Capability, error) {
|
||||
// this is a statement of the whole layer, so removing a key is done by leaving it out, which is
|
||||
// the only way removing one could work at all.
|
||||
func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, values map[string]any) error {
|
||||
return i.setSettings(ctx, nodeName, module, values, true)
|
||||
return i.setSettings(ctx, nodeName, module, values, true, "")
|
||||
}
|
||||
|
||||
// SetSettingsBy is SetSettings with who set the layer kept beside it (novox/hq ADR 0277): "approved by the
|
||||
// operator via telegram …" for a layer set on a warrant, the caller at the controller's terminal otherwise.
|
||||
// `settings` says it back, so a layer the operator approved on their phone is told from one typed.
|
||||
func (i *Inventory) SetSettingsBy(ctx context.Context, nodeName, module string, values map[string]any, setBy string) error {
|
||||
return i.setSettings(ctx, nodeName, module, values, true, setBy)
|
||||
}
|
||||
|
||||
// JudgeSettings judges a layer as SetSettings would, and keeps nothing: what a proposal is held to before the
|
||||
// operator is asked (novox/hq ADR 0277), so an ask is never raised for a layer the mesh would refuse.
|
||||
func (i *Inventory) JudgeSettings(ctx context.Context, nodeName, module string, values map[string]any) error {
|
||||
if err := i.judgeSettings(ctx, nodeName, module, values); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
given, err := givenIn(module, raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if nodeName == "" && len(given) > 0 {
|
||||
return fmt.Errorf("%s: %s is given per node — a port is a fact about one machine; "+
|
||||
"set it with --node", module, catalogue.PortsSetting)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeepSettings records a layer the mesh already holds, as it holds it, without judging it alone: for a
|
||||
@@ -807,10 +835,10 @@ func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, va
|
||||
// mesh-wide layer that needs a machine's own value to compose would be refused before that machine's
|
||||
// layer is there — though the mesh keeps both and composes. Composition still judges every layer.
|
||||
func (i *Inventory) KeepSettings(ctx context.Context, nodeName, module string, values map[string]any) error {
|
||||
return i.setSettings(ctx, nodeName, module, values, false)
|
||||
return i.setSettings(ctx, nodeName, module, values, false, "")
|
||||
}
|
||||
|
||||
func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, values map[string]any, judge bool) error {
|
||||
func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, values map[string]any, judge bool, setBy string) error {
|
||||
raw, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -848,9 +876,9 @@ func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, va
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
`insert into settings (node, module, values) values (null, $1, $2)
|
||||
`insert into settings (node, module, values, set_by) values (null, $1, $2, $3)
|
||||
on conflict (module) where node is null
|
||||
do update set values = excluded.values, set_at = now()`, module, raw); err != nil {
|
||||
do update set values = excluded.values, set_at = now(), set_by = excluded.set_by`, module, raw, nullable(setBy)); err != nil {
|
||||
return wrapModule(err, module)
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
@@ -878,9 +906,9 @@ func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, va
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx,
|
||||
`insert into settings (node, module, values) values ($1, $2, $3)
|
||||
`insert into settings (node, module, values, set_by) values ($1, $2, $3, $4)
|
||||
on conflict (node, module) where node is not null
|
||||
do update set values = excluded.values, set_at = now()`, node.ID, module, raw)
|
||||
do update set values = excluded.values, set_at = now(), set_by = excluded.set_by`, node.ID, module, raw, nullable(setBy))
|
||||
if err != nil {
|
||||
return wrapModule(err, module)
|
||||
}
|
||||
@@ -1069,6 +1097,11 @@ func wrapModule(err error, module string) error {
|
||||
|
||||
// ClearSettings removes a layer.
|
||||
func (i *Inventory) ClearSettings(ctx context.Context, nodeName, module string) error {
|
||||
return i.ClearSettingsBy(ctx, nodeName, module, "")
|
||||
}
|
||||
|
||||
// ClearSettingsBy removes a layer, and keeps who cleared it with the history copy (novox/hq ADR 0277).
|
||||
func (i *Inventory) ClearSettingsBy(ctx context.Context, nodeName, module, clearedBy string) error {
|
||||
nodeID, err := i.layerNode(ctx, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1082,6 +1115,17 @@ func (i *Inventory) ClearSettings(ctx context.Context, nodeName, module string)
|
||||
if _, err := tx.Exec(ctx, keepReplacedSQL, module, nodeID, "clear"); err != nil {
|
||||
return err
|
||||
}
|
||||
if clearedBy != "" {
|
||||
// The history copy says who cleared it, beside who had set it.
|
||||
if _, err := tx.Exec(ctx,
|
||||
`update settings_history set set_by = coalesce(set_by, '') || ' — cleared ' || $3
|
||||
where module = $1 and node is not distinct from $2::uuid
|
||||
and replaced_at = (select max(replaced_at) from settings_history
|
||||
where module = $1 and node is not distinct from $2::uuid)`,
|
||||
module, nodeID, clearedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
`delete from settings where module = $1 and node is not distinct from $2::uuid`, module, nodeID); err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- A trusted setting set on the operator's warrant (novox/hq ADR 0277): a layer says who set it.
|
||||
--
|
||||
-- Until now a layer carried only when it was set: a layer the operator approved on their phone and one typed
|
||||
-- at the controller's terminal looked the same, and `settings` could not say "approved by the operator via
|
||||
-- telegram". Kept with the layer, and with the history copy of it, so the provenance of a replaced layer is
|
||||
-- read back beside its values.
|
||||
alter table settings add column set_by text;
|
||||
alter table settings_history add column set_by text;
|
||||
@@ -44,6 +44,31 @@ type PastLayer struct {
|
||||
ReplacedAt time.Time
|
||||
// ReplacedBy is "set" or "clear".
|
||||
ReplacedBy string
|
||||
// SetBy is who had set the layer, when it was kept (novox/hq ADR 0277); empty for one set before that was kept.
|
||||
SetBy string
|
||||
}
|
||||
|
||||
// LayerOrigin is who set a layer and when (novox/hq ADR 0277): empty words for a layer set before who set it was
|
||||
// kept, and has false where there is no layer.
|
||||
func (i *Inventory) LayerOrigin(ctx context.Context, nodeName, module string) (setBy string, setAt time.Time, has bool, err error) {
|
||||
nodeID, err := i.layerNode(ctx, nodeName)
|
||||
if err != nil {
|
||||
return "", time.Time{}, false, err
|
||||
}
|
||||
var by *string
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select set_by, set_at from settings where module = $1 and node is not distinct from $2::uuid`,
|
||||
module, nodeID).Scan(&by, &setAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", time.Time{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", time.Time{}, false, err
|
||||
}
|
||||
if by != nil {
|
||||
setBy = *by
|
||||
}
|
||||
return setBy, setAt, true, nil
|
||||
}
|
||||
|
||||
// SettingsHistory is every layer of one module on one machine — the whole mesh's when nodeName is
|
||||
@@ -54,7 +79,7 @@ func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string
|
||||
return nil, err
|
||||
}
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select values, set_at, replaced_at, replaced_by from settings_history
|
||||
`select values, set_at, replaced_at, replaced_by, set_by from settings_history
|
||||
where module = $1 and node is not distinct from $2::uuid order by replaced_at desc`,
|
||||
module, nodeID)
|
||||
if err != nil {
|
||||
@@ -65,9 +90,13 @@ func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string
|
||||
for rows.Next() {
|
||||
var raw []byte
|
||||
var p PastLayer
|
||||
if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy); err != nil {
|
||||
var by *string
|
||||
if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy, &by); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if by != nil {
|
||||
p.SetBy = *by
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p.Values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -78,8 +107,8 @@ func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string
|
||||
|
||||
// keepReplacedSQL copies a layer into the history before it is replaced or cleared; run in the same
|
||||
// transaction as the write where there is one, so a write that fails leaves no history of it.
|
||||
const keepReplacedSQL = `insert into settings_history (node, module, values, set_at, replaced_by)
|
||||
select node, module, values, set_at, $3 from settings
|
||||
const keepReplacedSQL = `insert into settings_history (node, module, values, set_at, replaced_by, set_by)
|
||||
select node, module, values, set_at, $3, set_by from settings
|
||||
where module = $1 and node is not distinct from $2::uuid`
|
||||
|
||||
// layerNode is the node id of a layer, nil for the whole mesh's.
|
||||
|
||||
Reference in New Issue
Block a user