A node applying what it composed refuses a composition that stands in for anything its input lacks, the bus's user list alone held out (issue 1126, ADR 0547 §2)

This commit is contained in:
2026-10-26 18:59:05 +01:00
parent 6df0dfe208
commit b72de2b75a
4 changed files with 56 additions and 4 deletions
+40
View File
@@ -2,8 +2,10 @@ package compose
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -16,6 +18,26 @@ import (
// (mesh-hand-compose) gets that one. Each module's Data says whether its manifest declares data. The compositions are not
// signed and not yet by digest: the writer does both (delivery.SignComposed, Composed.ByDigest).
func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped []string, err error) {
return compositions(in, node, fps, mode, false)
}
// ErrStoodIn is a composition that holds a stand-in for a value the input does not carry (an own secret, a grant's
// sealed copy, a vault box, the certificate, the bus's user list) or a port it does not give: what a send would make,
// never what may be applied.
var ErrStoodIn = errors.New("compose: the composition stands in for what its input does not carry")
// BusUsersStandIn is what a composition says it stood in for when its input carries no bus user list.
const BusUsersStandIn = "the bus's user list"
// CompositionsApplied is Compositions for a node that applies what it composed itself (novox/hq ADR 0547 §2, issue
// 1126): a declaration that stands in for anything (Declared.StoodIn, Foreseen, ForeseenPorts) is refused with
// ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list
// (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself.
func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]delivery.Composed, []string, error) {
return compositions(in, node, fps, Reading, true)
}
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]delivery.Composed, skipped []string, err error) {
reading := in
reading.Nodes = append([]Node(nil), in.Nodes...)
found := false
@@ -35,6 +57,24 @@ func Compositions(in Input, node string, fps map[string]string, mode Mode) (out
if len(problems) > 0 {
return nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems)
}
if applied {
var stood []string
for _, x := range d.StoodIn {
// The bus's user list is the bus node's own to follow, written alone and never from a composition the node
// made itself (ADR 0547 §7, §8): its stand-in is held out of the apply, not applied.
if x != BusUsersStandIn {
stood = append(stood, x)
}
}
stood = append(stood, d.Foreseen...)
for _, p := range d.ForeseenPorts {
stood = append(stood, "port "+p)
}
if len(stood) > 0 {
sort.Strings(stood)
return nil, nil, fmt.Errorf("%w on %s: %s", ErrStoodIn, node, strings.Join(stood, ", "))
}
}
body, err := d.Body()
if err != nil {
return nil, nil, err
+11
View File
@@ -2,6 +2,7 @@ package compose
import (
"encoding/json"
"errors"
"strings"
"testing"
@@ -78,6 +79,16 @@ func TestAnOwnSecretHeldIsCarriedAndOneNotHeldIsForeseenOrLeftOut(t *testing.T)
t.Errorf("a held secret with no value is not named: %v", got.Rendering.Foreseen)
}
unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) })
// 1126 (ADR 0547 §2): a node applying what it composed itself never applies a stand-in: a held secret with no value in the
// Input is refused, naming it; held with its value, it composes.
fps := map[string]string{"assistant": "src2:" + strings.Repeat("a", 64), overlay.Name: "src2:" + strings.Repeat("b", 64)}
if _, _, err := CompositionsApplied(in, "laptop", fps); !errors.Is(err, ErrStoodIn) || !strings.Contains(err.Error(), "assistant/password") {
t.Errorf("a stand-in was not refused for a node's own apply: %v", err)
}
if out, _, err := CompositionsApplied(mk(), "laptop", fps); err != nil || len(out) == 0 {
t.Errorf("a held secret with its value is refused for a node's own apply: %v", err)
}
}
// An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228): nothing is composed or
+4 -3
View File
@@ -201,14 +201,15 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
return in, nil
}
// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.Compositions in the Reading
// mode at the fingerprints fps gives each module, as the controller composes them.
// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.CompositionsApplied at the
// fingerprints fps gives each module: composed in the Reading mode as the controller composes them, and refused when it
// stands in for anything the input does not carry (compose.ErrStoodIn), since the node applies what it composed.
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (map[string]delivery.Composed, []string, error) {
in, err := Input(ctx, r, mesh, s)
if err != nil {
return nil, nil, err
}
return compose.Compositions(in, s.Node, fps, compose.Reading)
return compose.CompositionsApplied(in, s.Node, fps)
}
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
+1 -1
View File
@@ -113,7 +113,7 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
r.BusUsers = c.in.BusUsers
if r.BusUsers == "" {
r.BusUsers = ForeseenBusUsers
foreseen("the bus's user list")
foreseen(BusUsersStandIn)
}
if len(c.in.BusMissing) > 0 {
out.Said = append(out.Said, fmt.Sprintf("the bus's user list leaves out %d user(s) the mesh has minted no credential "+