ADR 0627 §2: a holder's caller list per verb and its question to the builder are shared, not copied per holder
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -122,6 +122,32 @@ func TestACallerInTheArgumentsIsNeverTheCaller(t *testing.T) {
|
||||
if _, err := ParseCallers([]byte(`{"callers":["x"],"more":1}`)); err == nil {
|
||||
t.Error("a list with a field it does not know was read")
|
||||
}
|
||||
// A holder's list per verb (ADR 0627 §2): each verb's entries as above, plus every proved caller (AnyCaller) or
|
||||
// every operator's session (AnyPerson); an unproven caller never, a verb the holder does not serve refused.
|
||||
verbs, err := ParseVerbCallers(`{"publish":["mesh-build-go"],"versions":["*"],"release":["operator","sweep"]}`,
|
||||
[]string{"publish", "versions", "release", "remove"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
verb, caller string
|
||||
want bool
|
||||
}{{"publish", "ace.mesh-build-go", true}, {"publish", "ace.mesh-build-node", false}, {"versions", "g14.anything", true},
|
||||
{"versions", Unproven, false}, {"release", "person.jo", true}, {"release", "ace.operator", false},
|
||||
{"release", "novox.sweep", true}, {"remove", "novox.sweep", false}} {
|
||||
if got := verbs.Allowed(c.verb, c.caller) == nil; got != c.want {
|
||||
t.Errorf("%s by %q allowed: %v, want %v", c.verb, c.caller, got, c.want)
|
||||
}
|
||||
}
|
||||
if _, err := ParseVerbCallers(`{"mirror":["x"]}`, []string{"publish"}, nil); err == nil {
|
||||
t.Error("a list naming a verb the holder does not serve was read")
|
||||
}
|
||||
if d, _ := ParseVerbCallers(" ", nil, VerbCallers{"publish": {"x"}}); len(d["publish"]) != 1 {
|
||||
t.Error("an empty value is not the holder's defaults")
|
||||
}
|
||||
if ModuleOf("ace.mesh-build-go") != "mesh-build-go" {
|
||||
t.Error("ModuleOf does not read the module of a proved caller")
|
||||
}
|
||||
}
|
||||
|
||||
// A runtime that names no caller — one from before issue 365, or a call on the subject that names none — is
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -46,3 +47,66 @@ func (c Callers) Allows(caller string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// VerbCallers is a holder's trusted list per verb (novox/hq ADR 0627 §2, design 51 *Who may call which verb*): for each
|
||||
// verb of the seats it holds, the callers that may call it, each entry read as Callers reads it. Two entries more stand
|
||||
// for a whole class: AnyCaller lets every proved caller call the verb (a read the mesh keeps open), AnyPerson every
|
||||
// operator's session the runtime proved as person.<name>. It names no seat and no verb: the holder gives its verbs and
|
||||
// its defaults, and keeps any rule of its own seat (who may never be on a list) beside it.
|
||||
type VerbCallers map[string][]string
|
||||
|
||||
// The entries of a VerbCallers list that stand for a class of callers.
|
||||
const (
|
||||
AnyCaller = "*"
|
||||
AnyPerson = "operator"
|
||||
)
|
||||
|
||||
// ParseVerbCallers reads a holder's config value of verb to callers; an empty value is defaults. A verb not among verbs
|
||||
// is refused, so a list that names a verb the holder does not serve is said at once.
|
||||
func ParseVerbCallers(raw string, verbs []string, defaults VerbCallers) (VerbCallers, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return defaults, nil
|
||||
}
|
||||
var c VerbCallers
|
||||
if err := json.Unmarshal([]byte(raw), &c); err != nil {
|
||||
return nil, fmt.Errorf("callers is not a JSON object of verb to callers: %w", err)
|
||||
}
|
||||
for verb := range c {
|
||||
if !slices.Contains(verbs, verb) {
|
||||
return nil, fmt.Errorf("callers names the verb %q, which the holder does not serve", verb)
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Allowed answers nil when caller, as the runtime proved it, may call verb; else the refusal naming both. An unproven
|
||||
// caller is never allowed, not even by AnyCaller.
|
||||
func (c VerbCallers) Allowed(verb, caller string) error {
|
||||
if caller == "" || caller == Unproven {
|
||||
return fmt.Errorf("%s: refused: the request carries no caller the tool runner proved", verb)
|
||||
}
|
||||
person := strings.HasPrefix(caller, "person.")
|
||||
for _, entry := range c[verb] {
|
||||
switch entry {
|
||||
case AnyCaller:
|
||||
return nil
|
||||
case AnyPerson:
|
||||
if person {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
if (Callers{entry}).Allows(caller) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s: refused for %s: not on the holder's caller list for %s", verb, caller, verb)
|
||||
}
|
||||
|
||||
// ModuleOf is the module of a proved module caller, which the runtime names <machine>.<module>.
|
||||
func ModuleOf(proved string) string {
|
||||
if _, module, ok := strings.Cut(proved, "."); ok {
|
||||
return module
|
||||
}
|
||||
return proved
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -158,6 +160,21 @@ func TestBuilderBuildVersion(t *testing.T) {
|
||||
t.Errorf("DecodeBuilderBuild(%s) = %v", in, err)
|
||||
}
|
||||
}
|
||||
// A holder's question before it stores (ADR 0605 §4): asked at the builder's address with name, version and digest;
|
||||
// an answer without recorded is an error, never a yes.
|
||||
for answer, want := range map[string]string{`{"recorded":true}`: "true ", `{"recorded":false,"why":"other digest"}`: "false other digest",
|
||||
`{"name":"x"}`: "error", `not json`: "error"} {
|
||||
b := BuilderVersions{Ask: func(addr string, body any) (json.RawMessage, error) {
|
||||
if a := body.(map[string]any); addr != BuilderVersionsAddress || a["name"] != "n" || a["version"] != "0.1.2" || a["digest"] != "sha256:d" {
|
||||
t.Errorf("asked %s %v", addr, body)
|
||||
}
|
||||
return json.RawMessage(answer), nil
|
||||
}}
|
||||
ok, why, err := b.Recorded("n", "0.1.2", "sha256:d")
|
||||
if got := fmt.Sprintf("%v %s", ok, why); err != nil && want != "error" || err == nil && got != want {
|
||||
t.Errorf("answer %s: %s, %v; want %s", answer, got, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1358/AC6: the builder's bucket and module do not collide with the controller's build layout (buildrecord.go), which
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// The holder's question before it stores a publish (novox/hq ADR 0605 §4, design 51 *Publishing*): did the builder
|
||||
// number this version with this digest? Every registry seat's holder asks it the same way, so the asking lives here
|
||||
// once (ADR 0627 §2), beside the builder's layouts; no holder writes it again.
|
||||
//
|
||||
// The builder answers it today only as its module's tool builder_versions: no seat in the seat set names the builder's
|
||||
// role yet. Once one does, BuilderVersionsAddress becomes that seat's verb and nothing else changes for a holder.
|
||||
const BuilderVersionsAddress = BuilderModule + ".builder_versions"
|
||||
|
||||
// AskFunc is the runtime's request and reply over the bus (mesh-sdk stdio.Ask), given by the holder so this package does
|
||||
// not depend on the runtime.
|
||||
type AskFunc func(address string, body any) (json.RawMessage, error)
|
||||
|
||||
// BuilderVersions asks the builder whether it numbered a version with a digest. It answers the holder's Check:
|
||||
// recorded, and why not when it is not.
|
||||
type BuilderVersions struct{ Ask AskFunc }
|
||||
|
||||
// Recorded asks the builder about name@version with digest. An answer without `recorded` is an error, never a yes.
|
||||
func (b BuilderVersions) Recorded(name, version, digest string) (bool, string, error) {
|
||||
raw, err := b.Ask(BuilderVersionsAddress, map[string]any{"name": name, "version": version, "digest": digest})
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
var a struct {
|
||||
Recorded *bool `json:"recorded"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Recorded == nil {
|
||||
return false, "", fmt.Errorf("builder_versions answered no recorded: %s", raw)
|
||||
}
|
||||
return *a.Recorded, a.Why, nil
|
||||
}
|
||||
Reference in New Issue
Block a user