Copy the node-engine's validator into mesh-sdk, on the SDK's parser (issue 1353)

This commit is contained in:
2026-10-20 06:02:28 +00:00
parent 1d6a0e892e
commit be506bb502
3 changed files with 123 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
package validate
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/declaration"
)
// The SDK's validator speaks the copied parser's vocabulary version (issue 1353 AC3).
func TestVersionIsTheParsersVersion(t *testing.T) {
if Version != declaration.Version {
t.Fatalf("validate.Version = %d, declaration.Version = %d", Version, declaration.Version)
}
}
// A body of another declaration version is refused whole, by both the validator and the parser (AC3). The body is
// the shape mesh-host's validate_test.go uses for its good declaration, with the version changed.
func TestAnotherVersionIsRefusedWhole(t *testing.T) {
body := `{"declaration": 99, "resources": [{"id": "d", "type": "directory", "path": "/var/lib/x", "mode": "0755"}]}`
problems := Declaration([]byte(body))
if len(problems) == 0 {
t.Fatal("a declaration of another version was taken")
}
_, err := declaration.Parse([]byte(body))
var refused *declaration.RefusalError
if !errors.As(err, &refused) || strings.Join(refused.Problems, "\n") != strings.Join(problems, "\n") {
t.Fatalf("the parser and the validator disagree: %v / %v", err, problems)
}
}
+42
View File
@@ -0,0 +1,42 @@
// Package validate is the node-engine's own judgement of a declaration, for whoever must know before
// a declaration is sent whether a machine would take it (novox/hq to-be 45 §4, D1; §9, the merge gate).
//
// **One validator.** The controller composed declarations the host then refused whole — a manifest
// the catalogue check passed (novox/hq issue 236), a consumer's identity too long for the machine's
// names (issue 263) — because the only validator was the one the host runs as it applies. A second,
// written in the controller from the host's rules, would drift from them the first time either
// changed. So the host's own parsing is exported here, unchanged: what the controller's self-check and
// the merge gate run is what every machine runs.
//
// Copied from mesh-host at commit 619e14aa8e572c5056e3d2a2412c7f8b364bf840 (validate/validate.go), unchanged but for
// the import path of the parser, which is mesh-sdk's go/declaration (novox/hq ADR 0453 clause 3, ADR 0446 clause 4).
// mesh-host keeps its own copy until release 4b.
//
// It judges a declaration as it arrives over the link: actions are refused, as the host refuses them
// from the link (novox/hq ADR 0005). Nothing here touches a machine.
package validate
import (
"errors"
"git.novox.be/novox/mesh-sdk/go/declaration"
)
// Declaration is every problem the node-engine would refuse a declaration for, each in its own words;
// nil when it would take it. The body is the declaration as the controller composes it — the bytes a
// signature is made over — not the signed envelope.
func Declaration(raw []byte) []string {
_, err := declaration.Parse(raw)
if err == nil {
return nil
}
var refused *declaration.RefusalError
if errors.As(err, &refused) {
return refused.Problems
}
return []string{err.Error()}
}
// Version is the declaration vocabulary this validator speaks: a declaration of another version is
// refused whole by Declaration, as by the host.
const Version = declaration.Version
+50
View File
@@ -0,0 +1,50 @@
package validate
import (
"strings"
"testing"
)
// **The validator a controller imports is the host's**: a declaration the host takes passes, and one
// it refuses is refused with the host's own words, every problem at once.
func TestTheValidatorIsTheHosts(t *testing.T) {
good := `{"declaration": 1, "resources": [{"id": "d", "type": "directory", "path": "/var/lib/x", "mode": "0755"}]}`
if problems := Declaration([]byte(good)); problems != nil {
t.Fatalf("a declaration the host takes was refused: %v", problems)
}
// And one carrying the lease epoch and sequence the controller sends it under (novox/hq to-be 45
// §6): a controller validating with this package may send them.
ordered := `{"declaration": 1, "epoch": 57, "sequence": 3, "resources": [{"id": "d", "type": "directory", "path": "/var/lib/x", "mode": "0755"}]}`
if problems := Declaration([]byte(ordered)); problems != nil {
t.Fatalf("a declaration carrying its order was refused: %v", problems)
}
// And one carrying a file's owner-at-runtime and the files the provisioner fills (novox/hq ADR 0349 §5): the
// reader takes them before any controller sends them.
owned := `{"declaration": 1, "filled_files": ["/etc/x"], "resources": [{"id": "f", "type": "file", "path": "/etc/x", "content": "a", "owner-at-runtime": "owned"}]}`
if problems := Declaration([]byte(owned)); problems != nil {
t.Fatalf("a declaration saying who owns a file at runtime was refused: %v", problems)
}
// And one naming the provisioner client's account (novox/hq ADR 0349 §4).
named := `{"declaration": 1, "provisioner_account": "mesh-node-provisioner", "filled_files": ["/etc/x"], "resources": [{"id": "f", "type": "file", "path": "/etc/x", "content": "a", "owner-at-runtime": "owned"}]}`
if problems := Declaration([]byte(named)); problems != nil {
t.Fatalf("a declaration naming the provisioner client's account was refused: %v", problems)
}
for _, bad := range []string{
`{"declaration": 1, "provisioner_account": "root", "resources": [{"id": "f", "type": "file", "path": "/etc/x", "content": "a"}]}`,
`{"declaration": 1, "filled_files": ["/etc/none"], "resources": [{"id": "f", "type": "file", "path": "/etc/x", "content": "a", "owner-at-runtime": "owned"}]}`,
`{"declaration": 1, "resources": []}`,
`{"declaration": 99, "resources": [{"id": "d", "type": "directory", "path": "/x"}]}`,
`{"declaration": 1, "resources": [{"id": "d", "type": "nothing-the-host-knows"}]}`,
`not json`,
} {
problems := Declaration([]byte(bad))
if len(problems) == 0 {
t.Errorf("a declaration the host refuses was passed: %s", bad)
}
for _, p := range problems {
if strings.TrimSpace(p) == "" {
t.Errorf("a problem said nothing, for %s", bad)
}
}
}
}