The controller grants a read only from the owner's announcement (ADR 0495 clause 3). gitea announced `bound` unreadable, so mesh-provisioner bound no forge account; disk-load announced `readings` unreadable, refused to restic, nfs-server and mounts; restic announced `good` unreadable, which the self-check reads. Nothing in the catalogue compared readers with announcements since the gate's manifest check went (clause 5). Quick fix: mark those three states readable. Thorough fix: also checks/readable, which reads every owner's Go and TypeScript announcement and refuses any `reads` of a state it does not announce readable, so the next owner to announce cannot repeat it. Chose thorough: the same kind hit three owners at once. Reads of other repositories' owners (mesh-controller.lease, mesh-grants.accounts) are not judged here.
334 lines
9.1 KiB
Go
334 lines
9.1 KiB
Go
// Command readable refuses a catalogue where a module reads another module's state that the owner does not announce
|
|
// readable (novox/hq ADR 0495 clause 3, issue 1790): the controller grants a read only from the owner's announcement, so
|
|
// such a read is refused on the live mesh, said only in the controller's journal. The sweep of 2026-10-30 (issue 774)
|
|
// found three: gitea's `bound` (read by mesh-provisioner, so no forge account was bound), disk-load's `readings` (read by
|
|
// restic, nfs-server and mounts) and restic's `good` (read by the self-check). The merge gate's check of reads against
|
|
// the owner's manifest went with clause 5, and announcetest holds a module to its own list, not to its readers.
|
|
//
|
|
// An owner announces from its code. A Go module announces when its code imports mesh-sdk's go/announce: every
|
|
// announce.State literal there is read, its Name a string, a constant of the package, or a constant of mesh-sdk this
|
|
// check knows (Known). A TypeScript module announces when its code says the event `states` or calls the SDK's keeps:
|
|
// every `{ name: "...", readable: true }` there is read. An owner that announces nothing from code is read as before
|
|
// on the mesh, and is not judged; neither is an owner outside the catalogue (another repository's).
|
|
//
|
|
// go run . <catalog root>
|
|
//
|
|
// Exits 1 on a finding.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// Known are the mesh-sdk constants an announcement may name a state by, by the selector the code writes.
|
|
var Known = map[string]string{
|
|
"health.GoodState": health.GoodState,
|
|
"health.ReadingsState": health.ReadingsState,
|
|
}
|
|
|
|
const announcePkg = `"git.novox.be/novox/mesh-sdk/go/announce"`
|
|
|
|
// Announced is what one owner's code announces: each state, readable or not.
|
|
type Announced map[string]bool
|
|
|
|
// Findings is every refusal in the catalogue at root, sorted.
|
|
func Findings(root string) ([]string, error) {
|
|
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dirs := map[string]string{}
|
|
reads := map[string][]string{}
|
|
for _, path := range manifests {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m struct {
|
|
Module string `json:"module"`
|
|
Reads []json.RawMessage `json:"reads"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
dirs[m.Module] = filepath.Dir(path)
|
|
for _, r := range m.Reads {
|
|
var s string
|
|
if json.Unmarshal(r, &s) == nil {
|
|
reads[m.Module] = append(reads[m.Module], s)
|
|
}
|
|
}
|
|
}
|
|
var out []string
|
|
owners := map[string]Announced{}
|
|
for reader, rs := range reads {
|
|
for _, r := range rs {
|
|
owner, state, ok := strings.Cut(r, ".")
|
|
dir, inCatalogue := dirs[owner]
|
|
if !ok || !inCatalogue || owner == reader {
|
|
continue
|
|
}
|
|
a, seen := owners[owner]
|
|
if !seen {
|
|
var problems []string
|
|
a, problems, err = AnnouncedBy(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
owners[owner] = a
|
|
for _, p := range problems {
|
|
out = append(out, owner+": "+p)
|
|
}
|
|
}
|
|
if a == nil {
|
|
continue // announces nothing from code: read as before
|
|
}
|
|
readable, keeps := a[state]
|
|
switch {
|
|
case !keeps:
|
|
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code does not announce: the read is refused on the mesh", reader, owner, state, owner))
|
|
case !readable:
|
|
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code announces not readable: the read is refused on the mesh; announce it readable", reader, owner, state, owner))
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return dedupe(out), nil
|
|
}
|
|
|
|
func dedupe(in []string) []string {
|
|
var out []string
|
|
for i, s := range in {
|
|
if i == 0 || s != in[i-1] {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
var (
|
|
tsAnnounces = regexp.MustCompile(`emit\(\s*"states"|\bkeeps\(`)
|
|
tsState = regexp.MustCompile(`\{\s*name:\s*"([a-z0-9][a-z0-9-]*)"\s*(,\s*readable:\s*(true|false)\s*)?\}`)
|
|
)
|
|
|
|
// AnnouncedBy is what the module in dir announces from its code, nil when it announces nothing; problems are
|
|
// announcements whose state's name cannot be read.
|
|
func AnnouncedBy(dir string) (Announced, []string, error) {
|
|
var out Announced
|
|
var problems []string
|
|
pkgs := map[string][]*ast.File{}
|
|
fset := token.NewFileSet()
|
|
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
if n := d.Name(); path != dir && (n == "node_modules" || n == "dist" || n == "test" || n == "testdata" || strings.HasPrefix(n, ".")) {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
name := d.Name()
|
|
switch {
|
|
case strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go"):
|
|
f, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pkgs[filepath.Dir(path)] = append(pkgs[filepath.Dir(path)], f)
|
|
case strings.HasSuffix(name, ".ts") && !strings.HasSuffix(name, ".d.ts") && !strings.HasSuffix(name, ".test.ts"):
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !tsAnnounces.Match(raw) && !strings.Contains(string(raw), "KEPT") {
|
|
return nil
|
|
}
|
|
for _, m := range tsState.FindAllStringSubmatch(string(raw), -1) {
|
|
if out == nil {
|
|
out = Announced{}
|
|
}
|
|
out[m[1]] = out[m[1]] || m[3] == "true"
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, files := range pkgs {
|
|
imports := false
|
|
consts := map[string]ast.Expr{}
|
|
for _, f := range files {
|
|
for _, im := range f.Imports {
|
|
if im.Path.Value == announcePkg && (im.Name == nil || im.Name.Name == "announce") {
|
|
imports = true
|
|
}
|
|
}
|
|
for _, decl := range f.Decls {
|
|
g, ok := decl.(*ast.GenDecl)
|
|
if !ok || g.Tok != token.CONST {
|
|
continue
|
|
}
|
|
for _, spec := range g.Specs {
|
|
v := spec.(*ast.ValueSpec)
|
|
for i, n := range v.Names {
|
|
if i < len(v.Values) {
|
|
consts[n.Name] = v.Values[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !imports {
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = Announced{}
|
|
}
|
|
for _, f := range files {
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.CompositeLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
states := []*ast.CompositeLit{}
|
|
switch {
|
|
case isAnnounceState(lit.Type):
|
|
states = append(states, lit)
|
|
case isStateSlice(lit.Type):
|
|
for _, e := range lit.Elts {
|
|
if c, ok := e.(*ast.CompositeLit); ok && (c.Type == nil || isAnnounceState(c.Type)) {
|
|
states = append(states, c)
|
|
}
|
|
}
|
|
default:
|
|
return true
|
|
}
|
|
for _, s := range states {
|
|
name, readable, problem := stateOf(s, consts)
|
|
if problem != "" {
|
|
problems = append(problems, fmt.Sprintf("%s: %s", fset.Position(s.Pos()), problem))
|
|
continue
|
|
}
|
|
out[name] = out[name] || readable
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
}
|
|
return out, problems, nil
|
|
}
|
|
|
|
func isAnnounceState(e ast.Expr) bool {
|
|
s, ok := e.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
x, ok := s.X.(*ast.Ident)
|
|
return ok && x.Name == "announce" && s.Sel.Name == "State"
|
|
}
|
|
|
|
func isStateSlice(e ast.Expr) bool {
|
|
a, ok := e.(*ast.ArrayType)
|
|
return ok && isAnnounceState(a.Elt)
|
|
}
|
|
|
|
// stateOf is one announce.State literal's name and whether it is readable (Readable true, or Readers named).
|
|
func stateOf(lit *ast.CompositeLit, consts map[string]ast.Expr) (string, bool, string) {
|
|
var name string
|
|
var readable bool
|
|
named := false
|
|
for _, e := range lit.Elts {
|
|
kv, ok := e.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return "", false, "an announce.State without field names cannot be read; name its fields"
|
|
}
|
|
key, _ := kv.Key.(*ast.Ident)
|
|
if key == nil {
|
|
continue
|
|
}
|
|
switch key.Name {
|
|
case "Name":
|
|
v, ok := resolve(kv.Value, consts, 0)
|
|
if !ok {
|
|
return "", false, fmt.Sprintf("the state's name %s cannot be read: name it by a string, a constant of the package, or a mesh-sdk constant in checks/readable's Known", exprString(kv.Value))
|
|
}
|
|
name, named = v, true
|
|
case "Readable":
|
|
if id, ok := kv.Value.(*ast.Ident); ok {
|
|
readable = readable || id.Name == "true"
|
|
}
|
|
case "Readers":
|
|
readable = true
|
|
}
|
|
}
|
|
if !named {
|
|
return "", false, "an announce.State without a Name"
|
|
}
|
|
return name, readable, ""
|
|
}
|
|
|
|
func resolve(e ast.Expr, consts map[string]ast.Expr, depth int) (string, bool) {
|
|
if depth > 8 {
|
|
return "", false
|
|
}
|
|
switch v := e.(type) {
|
|
case *ast.BasicLit:
|
|
if v.Kind == token.STRING {
|
|
s, err := strconv.Unquote(v.Value)
|
|
return s, err == nil
|
|
}
|
|
case *ast.Ident:
|
|
if c, ok := consts[v.Name]; ok {
|
|
return resolve(c, consts, depth+1)
|
|
}
|
|
case *ast.SelectorExpr:
|
|
s, ok := Known[exprString(v)]
|
|
return s, ok
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func exprString(e ast.Expr) string {
|
|
switch v := e.(type) {
|
|
case *ast.Ident:
|
|
return v.Name
|
|
case *ast.SelectorExpr:
|
|
return exprString(v.X) + "." + v.Sel.Name
|
|
case *ast.BasicLit:
|
|
return v.Value
|
|
}
|
|
return fmt.Sprintf("%T", e)
|
|
}
|
|
|
|
func main() {
|
|
root := "."
|
|
if len(os.Args) > 1 {
|
|
root = os.Args[1]
|
|
}
|
|
found, err := Findings(root)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(2)
|
|
}
|
|
for _, f := range found {
|
|
fmt.Println(f)
|
|
}
|
|
if len(found) > 0 {
|
|
os.Exit(1)
|
|
}
|
|
}
|