From be506bb502d0eb0240a18b014afdd53b98d50340 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 06:02:28 +0000 Subject: [PATCH] Copy the node-engine's validator into mesh-sdk, on the SDK's parser (issue 1353) --- go/validate/sdk_test.go | 31 ++++++++++++++++++++++ go/validate/validate.go | 42 ++++++++++++++++++++++++++++++ go/validate/validate_test.go | 50 ++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 go/validate/sdk_test.go create mode 100644 go/validate/validate.go create mode 100644 go/validate/validate_test.go diff --git a/go/validate/sdk_test.go b/go/validate/sdk_test.go new file mode 100644 index 0000000..d946fff --- /dev/null +++ b/go/validate/sdk_test.go @@ -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) + } +} diff --git a/go/validate/validate.go b/go/validate/validate.go new file mode 100644 index 0000000..1291866 --- /dev/null +++ b/go/validate/validate.go @@ -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 diff --git a/go/validate/validate_test.go b/go/validate/validate_test.go new file mode 100644 index 0000000..51ade73 --- /dev/null +++ b/go/validate/validate_test.go @@ -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) + } + } + } +}