Compare commits
136
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
125929dbc4 | ||
|
|
6be8f984f7 | ||
|
|
fdc3331465 | ||
|
|
3498e54bdb | ||
|
|
2b340d7ddb | ||
|
|
b3261ccc03 | ||
|
|
da0573e124 | ||
|
|
2b8b47d862 | ||
|
|
e97167c58f | ||
|
|
0d6d7acaae | ||
|
|
dc0349a220 | ||
|
|
ccb7eaff13 | ||
|
|
e958de14d2 | ||
|
|
00d6fe81bf | ||
|
|
f28d3bf286 | ||
|
|
f735392cb9 | ||
|
|
d320a19a8c | ||
|
|
d8e43b48c4 | ||
|
|
64c893e2c2 | ||
|
|
bccaf75556 | ||
|
|
8f5611a2d1 | ||
|
|
1443adc4f0 | ||
|
|
dce7d41671 | ||
|
|
6f89e622df | ||
|
|
3b01711e1f | ||
|
|
1b485eb2aa | ||
|
|
1bdca045c2 | ||
|
|
70fd02e8da | ||
|
|
3079b7f115 | ||
|
|
dee54a1a13 | ||
|
|
0554f10c0a | ||
|
|
019184cf5d | ||
|
|
69ceac7fcf | ||
|
|
8acad77d33 | ||
|
|
46505d42d8 | ||
|
|
a198a959fe | ||
|
|
85d96d327f | ||
|
|
5601fdb51d | ||
|
|
d2f326dff4 | ||
|
|
8eb4465590 | ||
|
|
aef8b9c7cb | ||
|
|
7dd9e11239 | ||
|
|
aa5e2a0dd6 | ||
|
|
4a2555253c | ||
|
|
34d5c0d308 | ||
|
|
695ac0356d | ||
|
|
60a187cfa8 | ||
|
|
e9ba05bd9a | ||
|
|
10d53dd02b | ||
|
|
89d0eb13af | ||
|
|
69303e677a | ||
|
|
84ab1fe80d | ||
|
|
6061c775d5 | ||
|
|
8f5667cca5 | ||
|
|
79c2fc7fe1 | ||
|
|
55a88cdba4 | ||
|
|
29f6039fb1 | ||
|
|
b0a275c126 | ||
|
|
d31b41bb7e | ||
|
|
ed7c6c4fb6 | ||
|
|
cd66db54bf | ||
|
|
45688f196e | ||
|
|
d7214bb576 | ||
|
|
ee54bae09f | ||
|
|
3daedac1bb | ||
|
|
313c1997a4 | ||
|
|
e6806d5698 | ||
|
|
d0f384610c | ||
|
|
8d4b2cb1c6 | ||
|
|
4f433bdaf8 | ||
|
|
91cd016f65 | ||
|
|
cdddeee4ea | ||
|
|
3e019c5d7e | ||
|
|
fa4ccbc0a9 | ||
|
|
76aab05289 | ||
|
|
4b2d5aeaab | ||
|
|
68dd69f4ef | ||
|
|
3b61a05532 | ||
|
|
6be2e91da8 | ||
|
|
905c34a537 | ||
|
|
cc1a942235 | ||
|
|
93bfdd9e2f | ||
|
|
1f68f80760 | ||
|
|
ff4d042be5 | ||
|
|
4f24cb0751 | ||
|
|
0d86833123 | ||
|
|
715bc088c4 | ||
|
|
ab5f767372 | ||
|
|
40b1307839 | ||
|
|
a3a11842bf | ||
|
|
beba03613c | ||
|
|
d8e8c258a6 | ||
|
|
c73641c117 | ||
|
|
47b3ae7528 | ||
|
|
e5e3d7dce6 | ||
|
|
5adb8e1fea | ||
|
|
2bf608fc5f | ||
|
|
48624da9e3 | ||
|
|
25e5d00aba | ||
|
|
a4e3a9cbe0 | ||
|
|
dd120b1c90 | ||
|
|
6b929c9057 | ||
|
|
f49b6de79b | ||
|
|
4d05e0d290 | ||
|
|
972ecd105d | ||
|
|
7bf0e991d8 | ||
|
|
2bb964b000 | ||
|
|
692f18ef3b | ||
|
|
6128a570a2 | ||
|
|
c5dc10fa8c | ||
|
|
55eecc2d34 | ||
|
|
7fe569ef40 | ||
|
|
3e3294cd47 | ||
|
|
40e1e087e5 | ||
|
|
e1a182bf09 | ||
|
|
553cf50da7 | ||
|
|
bd9764ef9a | ||
|
|
bb85684b2a | ||
|
|
17ba95bca6 | ||
|
|
4aa77a4237 | ||
|
|
4d95d8fc1e | ||
|
|
d6c3fc1e40 | ||
|
|
285099a940 | ||
|
|
80a7cee6f3 | ||
|
|
e0d7644bb6 | ||
|
|
376a8b0fb5 | ||
|
|
6a39f43404 | ||
|
|
c973b31820 | ||
|
|
7f626147be | ||
|
|
080aed6855 | ||
|
|
4f67143a78 | ||
|
|
1572ab9b25 | ||
|
|
b0b7dfc5eb | ||
|
|
bf4ce7ef7b | ||
|
|
052fb473d0 | ||
|
|
bff850f798 |
@@ -7,6 +7,8 @@ modules/slack/cmd/slack-tools/slack-tools
|
||||
modules/jetbrains-toolbox/cmd/toolbox-tools/toolbox-tools
|
||||
modules/messenger/cmd/messenger/messenger
|
||||
modules/mesh-watcher/cmd/mesh-watcher/mesh-watcher
|
||||
modules/mesh-conditions/mesh-conditions
|
||||
modules/minio/object-store
|
||||
modules/claude-code/cmd/claude-code/claude-code
|
||||
modules/dnsmasq/cmd/dnsmasq-tools/dnsmasq-tools
|
||||
# ...and the same built from the module's root (go build ./cmd/<name>), which names it after the module.
|
||||
|
||||
@@ -2,4 +2,6 @@ module checks/credentialform
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Package gitignored refuses an ignore pattern that hides a module's source (catalogue review F1, and its kind): the
|
||||
// bare pattern `mesh-build-node` in a module's .gitignore, meant for the binary `go build` leaves, also matched the
|
||||
// directory cmd/mesh-build-node, so git ignored every new file there and publish.go was never committed. A path under
|
||||
// a module's cmd/ or internal/ is source: no .gitignore, the module's or the catalogue's, may ignore one, tracked or
|
||||
// new. A binary is ignored by an anchored pattern (`/mesh-build-node`, `/cmd/mesh-build-node/mesh-build-node`).
|
||||
package gitignored
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Probe is the file name asked of each source directory: a Go file a change might add there.
|
||||
const Probe = "added_by_a_change.go"
|
||||
|
||||
// Hidden is one source path an ignore pattern hides, and the pattern (its file, line and text, as git names it).
|
||||
type Hidden struct {
|
||||
Path, Pattern string
|
||||
}
|
||||
|
||||
// Sources are the source directories of root's modules, relative to root: every directory under modules/*/cmd and
|
||||
// modules/*/internal (node_modules left out).
|
||||
func Sources(root string) ([]string, error) {
|
||||
mods, err := filepath.Glob(filepath.Join(root, "modules", "*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, m := range mods {
|
||||
for _, top := range []string{"cmd", "internal"} {
|
||||
base := filepath.Join(m, top)
|
||||
if st, err := os.Stat(base); err != nil || !st.IsDir() {
|
||||
continue
|
||||
}
|
||||
err := filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if d.Name() == "node_modules" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
rel, _ := filepath.Rel(root, p)
|
||||
out = append(out, filepath.ToSlash(rel))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// isSource is whether a file is source a module's build reads: Go and TypeScript files and a Go module's own files.
|
||||
// What a build or a test script leaves beside them (a binary, compiled JavaScript) may be ignored.
|
||||
func isSource(name string) bool {
|
||||
switch filepath.Ext(name) {
|
||||
case ".go", ".ts", ".tsx":
|
||||
return true
|
||||
}
|
||||
return name == "go.mod" || name == "go.sum"
|
||||
}
|
||||
|
||||
// Find asks git which of the source paths root's ignore files hide: each source directory, a Go file added in it, and
|
||||
// every source file there (isSource). It reads the .gitignore files of root's tree through a repository of its own, so it judges a git
|
||||
// checkout and a plain folder alike, and no one's global ignores count.
|
||||
func Find(root string, dirs []string) ([]Hidden, error) {
|
||||
gitDir, err := os.MkdirTemp("", "gitignored-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(gitDir)
|
||||
if out, err := exec.Command("git", "init", "-q", "--bare", gitDir).CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("git init: %v: %s", err, out)
|
||||
}
|
||||
var ask bytes.Buffer
|
||||
for _, d := range dirs {
|
||||
ask.WriteString(d + "/\x00" + d + "/" + Probe + "\x00")
|
||||
entries, err := os.ReadDir(filepath.Join(root, d))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && isSource(e.Name()) {
|
||||
ask.WriteString(d + "/" + e.Name() + "\x00")
|
||||
}
|
||||
}
|
||||
}
|
||||
abs, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command("git", "--git-dir="+gitDir, "--work-tree="+abs, "-c", "core.excludesFile=/dev/null",
|
||||
"check-ignore", "--no-index", "-v", "-z", "--stdin")
|
||||
cmd.Dir = abs
|
||||
cmd.Stdin = &ask
|
||||
out, err := cmd.Output()
|
||||
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
|
||||
return nil, nil // nothing is ignored
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git check-ignore: %w", err)
|
||||
}
|
||||
// -v -z answers source, line, pattern and path, each ended by NUL.
|
||||
f := strings.Split(strings.TrimRight(string(out), "\x00"), "\x00")
|
||||
var hidden []Hidden
|
||||
for i := 0; i+3 < len(f); i += 4 {
|
||||
if f[i+2] == "" || strings.HasPrefix(f[i+2], "!") {
|
||||
continue // a negation keeps the path
|
||||
}
|
||||
hidden = append(hidden, Hidden{Path: f[i+3], Pattern: fmt.Sprintf("%s:%s: %s", f[i], f[i+1], f[i+2])})
|
||||
}
|
||||
return hidden, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package gitignored
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The kind of catalogue review F1: a bare pattern hides the command directory it shares a name with; an anchored one
|
||||
// hides the binary alone; a glob that ignores source is refused too; a negation keeps a path.
|
||||
func TestFindNamesEachPatternThatHidesSource(t *testing.T) {
|
||||
for name, c := range map[string]struct {
|
||||
ignore string
|
||||
hidden []string
|
||||
}{
|
||||
"a bare name hides cmd/<name>": {"nfs-server\n", []string{"modules/m/cmd/nfs-server/", "modules/m/cmd/nfs-server/" + Probe, "modules/m/cmd/nfs-server/main.go"}},
|
||||
"anchored to the binary": {"/nfs-server\n/cmd/nfs-server/nfs-server\n", nil},
|
||||
"a glob ignoring Go source": {"*.go\n!main.go\n", []string{"modules/m/cmd/" + Probe, "modules/m/cmd/nfs-server/" + Probe,
|
||||
"modules/m/internal/" + Probe, "modules/m/internal/x/" + Probe, "modules/m/internal/x/x.go"}},
|
||||
"a pattern over internal/": {"internal/\n", []string{"modules/m/internal/", "modules/m/internal/" + Probe, "modules/m/internal/x/",
|
||||
"modules/m/internal/x/" + Probe, "modules/m/internal/x/x.go"}},
|
||||
"nothing but generated JavaScript": {"*.js\n", nil},
|
||||
} {
|
||||
root := t.TempDir()
|
||||
for f, b := range map[string]string{
|
||||
"modules/m/.gitignore": c.ignore,
|
||||
"modules/m/cmd/nfs-server/main.go": "package main\n",
|
||||
"modules/m/cmd/nfs-server/run.js": "",
|
||||
"modules/m/internal/x/x.go": "package x\n",
|
||||
"modules/m/node_modules/y/index.go": "",
|
||||
"modules/other/cmd/other/other.go": "package main\n",
|
||||
} {
|
||||
p := filepath.Join(root, f)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(b), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
dirs, err := Sources(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := Find(root, dirs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var paths []string
|
||||
for _, h := range got {
|
||||
paths = append(paths, h.Path)
|
||||
if !strings.HasPrefix(h.Pattern, "modules/m/.gitignore:") {
|
||||
t.Errorf("%s: %s is named hidden by %q, want the module's .gitignore", name, h.Path, h.Pattern)
|
||||
}
|
||||
}
|
||||
if strings.Join(paths, ",") != strings.Join(c.hidden, ",") {
|
||||
t.Errorf("%s: hidden %v, want %v", name, paths, c.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The catalogue's own tree: no ignore pattern hides a path under a module's cmd/ or internal/.
|
||||
func TestNoIgnorePatternHidesAModulesSource(t *testing.T) {
|
||||
root := filepath.Join("..", "..")
|
||||
dirs, err := Sources(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
t.Fatal("no module source directory found: the check reads nothing")
|
||||
}
|
||||
hidden, err := Find(root, dirs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, h := range hidden {
|
||||
t.Errorf("%s is hidden by %s: a module's source is never ignored; anchor the pattern to the binary (/<name>, /cmd/<name>/<name>)", h.Path, h.Pattern)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module git.novox.be/novox/mesh-catalog/checks/gitignored
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,3 @@
|
||||
module git.novox.be/novox/mesh-catalog/checks/gofmt
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package gofmt refuses a Go file of the catalogue that gofmt would change (the second of a kind in one milestone:
|
||||
// buildstep's generated.go and claude-code's config_test.go reached a join unformatted). A check here holds every
|
||||
// module to it, where a module's own tests hold only themselves.
|
||||
package gofmt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/format"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Files are the Go files under root, relative to it: every .go file but those in .git, node_modules and a testdata
|
||||
// directory (a test's fixture may be anything).
|
||||
func Files(root string) ([]string, error) {
|
||||
var out []string
|
||||
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
switch d.Name() {
|
||||
case ".git", "node_modules", "testdata":
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(d.Name(), ".go") && d.Type().IsRegular() {
|
||||
rel, _ := filepath.Rel(root, p)
|
||||
out = append(out, filepath.ToSlash(rel))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Unformatted are the files gofmt would change, and why for one that does not parse.
|
||||
func Unformatted(root string, files []string) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
for _, f := range files {
|
||||
src, err := os.ReadFile(filepath.Join(root, f))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
got, err := format.Source(src)
|
||||
switch {
|
||||
case err != nil:
|
||||
out[f] = "it does not parse: " + err.Error()
|
||||
case !bytes.Equal(got, src):
|
||||
out[f] = "gofmt changes it: run gofmt -w " + f
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package gofmt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnformattedNamesWhatGofmtChanges(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for f, src := range map[string]string{
|
||||
"modules/a/a.go": "package a\n",
|
||||
"modules/b/b.go": "package b\nimport (\n\t\"os\"\n\t\"fmt\"\n)\nvar _, _ = fmt.Sprint, os.Exit\n",
|
||||
"modules/c/testdata/c.go": "package c",
|
||||
"modules/d/node_modules/d.go": "package d",
|
||||
} {
|
||||
p := filepath.Join(root, f)
|
||||
_ = os.MkdirAll(filepath.Dir(p), 0o755)
|
||||
if err := os.WriteFile(p, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
files, err := Files(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := Unformatted(root, files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 2 || len(got) != 1 || got["modules/b/b.go"] == "" {
|
||||
t.Errorf("files %v, unformatted %v; want a.go and b.go read, b.go named", files, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The catalogue's own Go files are as gofmt writes them.
|
||||
func TestTheCataloguesGoIsFormatted(t *testing.T) {
|
||||
root := filepath.Join("..", "..")
|
||||
files, err := Files(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatal("no Go file found: the check reads nothing")
|
||||
}
|
||||
got, err := Unformatted(root, files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for f, why := range got {
|
||||
t.Errorf("%s: %s", f, why)
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,6 @@ module checks/health
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
|
||||
@@ -2,4 +2,6 @@ module checks/manifests
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package manifests reads every module's manifest as the catalogue's tree holds it and refuses what no judge could
|
||||
// read: a module.json that does not decode, and two manifests that name one module.
|
||||
// read: a module.json that does not decode, a module.json giving one field twice, and two manifests that name one module.
|
||||
//
|
||||
// It judges no manifest (novox/hq ADR 0578 §2): the checker judges every change's manifests with the indexer's judge, the
|
||||
// one judge, so the field, seat, bus and catalogue rules are said once, there. Until 2026-11 this check parsed each
|
||||
@@ -9,6 +9,8 @@
|
||||
package manifests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -18,7 +20,9 @@ import (
|
||||
)
|
||||
|
||||
// Problems is what is wrong with the tree's manifests under root/modules, one line each: a module.json whose bytes are
|
||||
// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), then a module named by two manifests.
|
||||
// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), one giving a field twice (a decoder keeps the
|
||||
// last and drops the first without a word: a merge of two branches that each added the field writes it so), then a module
|
||||
// named by two manifests.
|
||||
func Problems(root string) ([]string, error) {
|
||||
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||
if err != nil {
|
||||
@@ -38,6 +42,10 @@ func Problems(root string) ([]string, error) {
|
||||
out = append(out, fmt.Sprintf("%s: %v", rel, err))
|
||||
continue
|
||||
}
|
||||
if twice := givenTwice(raw); twice != "" {
|
||||
out = append(out, fmt.Sprintf("%s: the field %s is given twice; a judge reads only the last", rel, twice))
|
||||
continue
|
||||
}
|
||||
if named[m.Module] {
|
||||
out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module))
|
||||
continue
|
||||
@@ -46,3 +54,47 @@ func Problems(root string) ([]string, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// givenTwice is the first field an object of raw gives twice, as its path (`invokes`, `build.artifacts[0].env`), or empty.
|
||||
func givenTwice(raw []byte) string {
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
var walk func(path string) string
|
||||
walk = func(path string) string {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
switch t {
|
||||
case json.Delim('{'):
|
||||
seen := map[string]bool{}
|
||||
for d.More() {
|
||||
k, err := d.Token()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
key, _ := k.(string)
|
||||
at := key
|
||||
if path != "" {
|
||||
at = path + "." + key
|
||||
}
|
||||
if seen[key] {
|
||||
return at
|
||||
}
|
||||
seen[key] = true
|
||||
if twice := walk(at); twice != "" {
|
||||
return twice
|
||||
}
|
||||
}
|
||||
_, _ = d.Token()
|
||||
case json.Delim('['):
|
||||
for i := 0; d.More(); i++ {
|
||||
if twice := walk(fmt.Sprintf("%s[%d]", path, i)); twice != "" {
|
||||
return twice
|
||||
}
|
||||
}
|
||||
_, _ = d.Token()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return walk("")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// The tree's manifests are ones a judge can read, and no module is named twice. A row is a catalogue of its own: a
|
||||
// manifest that is no JSON and two manifests of one module are refused, naming the file; a manifest with a field this
|
||||
// manifest that is no JSON, one giving a field twice and two manifests of one module are refused, naming the file; a manifest with a field this
|
||||
// check does not know is not (it judges nothing: the checker does, ADR 0578 §2). The catalogue itself passes.
|
||||
func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) {
|
||||
for _, row := range []struct {
|
||||
@@ -21,6 +21,10 @@ func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) {
|
||||
{name: "a manifest the judge can read", files: map[string]string{"a": `{"module":"a","version":"1"}`}},
|
||||
{name: "a field no part knows is the judge's to refuse", files: map[string]string{"a": `{"module":"a","version":"1","unheard":true}`}},
|
||||
{name: "bytes that are no manifest", files: map[string]string{"a": `{"module":`}, refused: "modules/a/module.json"},
|
||||
{name: "a field given twice", files: map[string]string{"a": `{"module":"a","version":"1","invokes":["x.y"],"build":{"artifacts":[{}]},"invokes":["z.w"]}`},
|
||||
refused: "modules/a/module.json: the field invokes is given twice"},
|
||||
{name: "a field given twice deeper", files: map[string]string{"a": `{"module":"a","version":"1","build":{"artifacts":[{"name":"t","name":"u"}]}}`},
|
||||
refused: "the field build.artifacts[0].name is given twice"},
|
||||
{name: "two manifests of one module", files: map[string]string{"a": `{"module":"x","version":"1"}`, "b": `{"module":"x","version":"1"}`},
|
||||
refused: "two manifests name one module"},
|
||||
} {
|
||||
|
||||
@@ -2,4 +2,6 @@ module checks/readable
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
58
|
||||
57
|
||||
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
|
||||
rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and
|
||||
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
|
||||
|
||||
@@ -2,10 +2,10 @@ module adwaita
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -8,10 +8,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"artifactstoretools/internal/registry"
|
||||
)
|
||||
|
||||
@@ -45,10 +48,22 @@ func DoorAt(file string) func() (Door, error) {
|
||||
}
|
||||
|
||||
// runtimeBus is the holder's bus through the runtime: its reads, its findings and its seat's event.
|
||||
type runtimeBus struct{}
|
||||
type runtimeBus struct{ views *views.Reader }
|
||||
|
||||
func (runtimeBus) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
||||
func (runtimeBus) Get(state, key string) ([]byte, bool, error) {
|
||||
// followed says whether the state is the indexer's or the assigner's, which the holder asks once and follows by events
|
||||
// (ADR 0615) rather than reads.
|
||||
func (b runtimeBus) followed(state string) bool { return b.views != nil && b.views.Follows(state) }
|
||||
|
||||
func (b runtimeBus) Keys(state string) ([]string, error) {
|
||||
if b.followed(state) {
|
||||
return b.views.Keys(state)
|
||||
}
|
||||
return stdio.State(state).Keys()
|
||||
}
|
||||
func (b runtimeBus) Get(state, key string) ([]byte, bool, error) {
|
||||
if b.followed(state) {
|
||||
return b.views.Get(state, key)
|
||||
}
|
||||
e, err := stdio.State(state).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
@@ -82,7 +97,7 @@ func (runtimeBus) Publish(subject string, body any) error {
|
||||
|
||||
// follow watches the builder's done keys and the wants: a build's end sweeps and
|
||||
// observes again; a want changed observes again. Event-driven: nothing reads them on a timer.
|
||||
func follow(h *Holder) {
|
||||
func follow(h *Holder, v *views.Reader) {
|
||||
<-stdio.Served()
|
||||
watch := func(state string, handle func(stdio.StateChange) error) {
|
||||
for {
|
||||
@@ -111,11 +126,22 @@ func follow(h *Holder) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
watch(WantsRead, func(c stdio.StateChange) error {
|
||||
if !c.Current {
|
||||
go observe()
|
||||
// The wants are the assigner's: the first view then its events (ADR 0615). What Watch hands over before it returns
|
||||
// is the first view, not a change.
|
||||
var current atomic.Bool
|
||||
current.Store(true)
|
||||
for {
|
||||
err := v.Watch(WantsRead, func(string, []byte, bool) {
|
||||
if !current.Load() {
|
||||
go observe()
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
return nil
|
||||
})
|
||||
h.logf("watching %s: %v; asked again in 10s", WantsRead, err)
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
current.Store(false)
|
||||
observe()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"artifactstoretools/internal/registry"
|
||||
)
|
||||
|
||||
@@ -903,6 +905,12 @@ func (h *Holder) Observe(ctx context.Context) error {
|
||||
continue // the sweep's own finding, which the sweep clears
|
||||
}
|
||||
if _, still := now[k]; !still {
|
||||
// A finding of a first view unanswered is the reader's (views.Reader), cleared by its answer, not by a sweep.
|
||||
if raw, found, err := h.Bus.Get(FindingsState, k); err == nil && found {
|
||||
if f, err := health.DecodeFinding(raw); err == nil && f.Kind == views.UnansweredKind {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := h.Bus.Delete(FindingsState, k); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,12 +10,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
|
||||
@@ -26,13 +30,23 @@ func main() {
|
||||
Run: ExecRunner,
|
||||
UID: os.Getuid(),
|
||||
}
|
||||
h := &Holder{Bus: runtimeBus{}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
|
||||
v := &views.Reader{Module: "artifact-store-tools", Logf: logf, Findings: views.OwnFindings{}, Acts: handacts.Recorder{Ask: stdio.Ask}}
|
||||
v.Follow(delivery.AssignerModule, "wants", WantsTool)
|
||||
v.Follow(delivery.IndexerModule, "manifests", ManifestsTool)
|
||||
h := &Holder{Bus: runtimeBus{views: v}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
|
||||
Seats: stdio.Ask, Now: time.Now, Logf: logf}
|
||||
h.Acts = handacts.Recorder{Ask: stdio.Ask}
|
||||
go follow(h)
|
||||
v.Start(context.Background())
|
||||
go follow(h, v)
|
||||
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): artifact-store-tools.
|
||||
if err := stdio.Serve("", append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...)); err != nil {
|
||||
if err := stdio.Serve("", append(append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...), v.Tools("artifact_store")...)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// The owners' read tools of the states the holder follows, which the manifest invokes (ADR 0615).
|
||||
const (
|
||||
WantsTool = delivery.AssignerModule + ".assigner_read_wants"
|
||||
ManifestsTool = delivery.IndexerModule + ".indexer_read_manifests"
|
||||
)
|
||||
|
||||
@@ -2,12 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -443,10 +445,11 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Tools []string `json:"tools"`
|
||||
Invokes []string `json:"invokes"`
|
||||
Reads []string `json:"reads"`
|
||||
Claims []struct {
|
||||
Tools []string `json:"tools"`
|
||||
Invokes []string `json:"invokes"`
|
||||
Reads []string `json:"reads"`
|
||||
Consumes []string `json:"consumes"`
|
||||
Claims []struct {
|
||||
Name string `json:"name"`
|
||||
Serves []string `json:"serves"`
|
||||
} `json:"claims"`
|
||||
@@ -455,7 +458,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var served []string
|
||||
for _, tl := range Tools(&Store{}, recordsAsked{&asked{}}, &Catalogue{}) {
|
||||
for _, tl := range append(Tools(&Store{}, recordsAsked{&asked{}}, &Catalogue{}), (&views.Reader{}).Tools("artifact_store")...) {
|
||||
if !strings.HasPrefix(tl.Name, "artifact_store_") || tl.Description == "" || tl.Run == nil {
|
||||
t.Errorf("tool %q", tl.Name)
|
||||
}
|
||||
@@ -470,12 +473,17 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
// ADR 0571 clause 2: it holds the seat mesh-artifact-sweep and serves its verbs, and calls no verb of the controller's;
|
||||
// it reads no controller state, the lease neither (issue 1126).
|
||||
// CAT-AS1: what it holds and removes it asks the two registry seats the artifact store's holder serves.
|
||||
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule,
|
||||
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule, WantsTool, ManifestsTool,
|
||||
"seat:oci-artifact-registry.hold", "seat:oci-artifact-registry.release", "seat:oci-artifact-registry.remove",
|
||||
"seat:container-image-registry.hold", "seat:container-image-registry.release", "seat:container-image-registry.remove"}
|
||||
if !reflect.DeepEqual(m.Invokes, want) {
|
||||
t.Errorf("invokes %v", m.Invokes)
|
||||
}
|
||||
// ADR 0615: the assigner's wants and the indexer's manifests are asked once and followed by their events, not read.
|
||||
if !reflect.DeepEqual(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.views.manifests"}) ||
|
||||
slices.Contains(m.Reads, WantsRead) || slices.Contains(m.Reads, ManifestsRead) {
|
||||
t.Errorf("consumes %v, reads %v", m.Consumes, m.Reads)
|
||||
}
|
||||
var verbs []string
|
||||
for _, tl := range SeatTools(&Holder{}, &Store{}) {
|
||||
verbs = append(verbs, strings.TrimPrefix(tl.Name, catalogue.ArtifactSweepSeat+"."))
|
||||
|
||||
@@ -3,22 +3,24 @@ module artifactstoretools
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639
|
||||
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.2 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639 h1:5HeZzk6tKzcSwVivD3IcIvQ/2WIvysZvNvNKyq/oIBA=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639 h1:+bkN8op2yrAlBYrOB+hYnIoIZkbjXsNmc/4//uMnL4E=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
@@ -23,5 +29,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-catalog.modules",
|
||||
"seat:mesh-catalog.module",
|
||||
"mesh-delivery-assigner.assigner_read_wants",
|
||||
"mesh-delivery-indexer.indexer_read_manifests",
|
||||
"seat:oci-artifact-registry.hold",
|
||||
"seat:oci-artifact-registry.release",
|
||||
"seat:oci-artifact-registry.remove",
|
||||
@@ -29,12 +31,14 @@
|
||||
"seat:container-image-registry.remove"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-delivery-assigner.wants",
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-builder.build-log",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"node-engine.have"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-assigner.views.wants",
|
||||
"mesh-delivery-indexer.views.manifests"
|
||||
],
|
||||
"state": [
|
||||
"findings"
|
||||
],
|
||||
@@ -42,7 +46,9 @@
|
||||
"artifact_store_repositories",
|
||||
"artifact_store_usage",
|
||||
"artifact_store_references",
|
||||
"artifact_store_collect"
|
||||
"artifact_store_collect",
|
||||
"artifact_store_first_views",
|
||||
"artifact_store_first_view_again"
|
||||
],
|
||||
"config": {
|
||||
"older-store-names": {
|
||||
|
||||
@@ -2,10 +2,10 @@ module asuszephyrusg14
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module avahi
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module blueman
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module bluetooth
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -3,20 +3,21 @@ module brother
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.2 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
)
|
||||
|
||||
+12
-10
@@ -1,15 +1,17 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
@@ -23,5 +25,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
|
||||
@@ -269,12 +269,18 @@ func TestTheManifestListsEveryToolServed(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Tools []string `json:"tools"`
|
||||
Invokes []string `json:"invokes"`
|
||||
Tools []string `json:"tools"`
|
||||
Invokes []string `json:"invokes"`
|
||||
Reads []string `json:"reads"`
|
||||
Consumes []string `json:"consumes"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted"}) {
|
||||
t.Errorf("invokes = %v, want the hand-act seat's warranted alone", m.Invokes)
|
||||
// The hand-act seat's warranted and hand-act, and the owners' read tools of the three followed states; no verb of
|
||||
// the controller, and no read of the indexer's or the assigner's state (ADR 0615 §1): their views are consumed.
|
||||
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.hand-act", ModulesTool, ManifestsTool, PlacementTool}) ||
|
||||
!slices.Equal(m.Reads, []string{"claude-licence-manager.bindings"}) ||
|
||||
!slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests", "mesh-delivery-assigner.views.placement"}) {
|
||||
t.Errorf("invokes = %v, reads = %v, consumes = %v", m.Invokes, m.Reads, m.Consumes)
|
||||
}
|
||||
if src, _ := os.ReadFile("proposals.go"); !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) ||
|
||||
strings.Contains(string(src), "seat:mesh-controller.warranted") {
|
||||
@@ -285,6 +291,7 @@ func TestTheManifestListsEveryToolServed(t *testing.T) {
|
||||
listed[n] = true
|
||||
}
|
||||
p, _ := node(t, "laptop")
|
||||
meshViews = newMeshViews()
|
||||
served := tools(p, nil, NewServerView(p), memConfig{}, NewConfigView(p))
|
||||
for _, tool := range served {
|
||||
if !listed[tool.Name] {
|
||||
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// ForgeProvision is the provision whose credential this is, and the file this module's manifest has it delivered to.
|
||||
@@ -55,16 +56,16 @@ func (f *ForgeHealth) key() string { return "claude-code.machine." + f.Node + ".
|
||||
|
||||
// Look is the condition to say after one placing of the credential, nil when nothing changed: raised on a failure, again
|
||||
// when its reason changes, cleared when it places again; on the first look a success clears what a run before raised.
|
||||
func (f *ForgeHealth) Look(err error) *asks.Condition {
|
||||
func (f *ForgeHealth) Look(err error) *oc.Condition {
|
||||
first := !f.looked
|
||||
f.looked = true
|
||||
subject := asks.Scope{Scope: "machine", ID: f.Node, Machine: f.Node}
|
||||
subject := oc.ConditionSubject{Scope: "machine", ID: f.Node, Machine: f.Node}
|
||||
if err == nil {
|
||||
if f.raised == "" && !first {
|
||||
return nil
|
||||
}
|
||||
f.raised = ""
|
||||
return &asks.Condition{Key: f.key(), Subject: subject, State: asks.StateCleared}
|
||||
return &oc.Condition{Key: f.key(), Subject: subject, State: occ.StateCleared}
|
||||
}
|
||||
why := err.Error()
|
||||
if why == f.raised {
|
||||
@@ -72,10 +73,10 @@ func (f *ForgeHealth) Look(err error) *asks.Condition {
|
||||
}
|
||||
f.raised = why
|
||||
needs := "read claude-code's journal on " + f.Node + " and the owner of its forge credential file"
|
||||
return &asks.Condition{Key: f.key(), Severity: asks.SeverityWarning, State: asks.StateRaised, Subject: subject,
|
||||
return &oc.Condition{Key: f.key(), Severity: occ.SeverityWarning, State: occ.StateRaised, Subject: subject,
|
||||
Headline: "Agents on " + f.Node + " cannot use the forge credential",
|
||||
Summary: why, Needs: needs,
|
||||
Explanation: asks.NeedsYou + needs + ". " + sentence(why) + " Until it is placed, an agent there cannot " +
|
||||
Explanation: occ.NeedsYou + needs + ". " + sentence(why) + " Until it is placed, an agent there cannot " +
|
||||
"clone or fetch the mesh's private repositories.",
|
||||
Resolved: "The forge's read credential is placed again on " + f.Node + "."}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
const agentToken = "0123456789abcdef0123456789abcdef01234567"
|
||||
@@ -316,7 +316,7 @@ func TestTheForgeCredentialIsTheRuntimesAndItsFailureIsAConditionOfTheModule(t *
|
||||
got := ""
|
||||
if c != nil {
|
||||
got = c.State
|
||||
if _, err := asks.EncodeCondition(*c); err != nil {
|
||||
if _, err := occ.EncodeCondition(*c); err != nil {
|
||||
t.Errorf("look %d: the condition is refused: %v", i, err)
|
||||
}
|
||||
if c.Key != "claude-code.machine.anchor.forge-credential" {
|
||||
|
||||
@@ -9,25 +9,25 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats"
|
||||
)
|
||||
|
||||
// meshForTheGuard is a mesh of two machines and the verbs that replace what an agent runs over ssh, as the
|
||||
// seats' contract and the modules' manifests say them.
|
||||
func meshForTheGuard() GuardData {
|
||||
seats := []catalogue.Seat{
|
||||
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
|
||||
defined := []seats.Seat{
|
||||
{Name: "mesh-placement", Scope: "mesh", Serves: []seats.Verb{
|
||||
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
|
||||
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
|
||||
{Name: "node-service-manager", Scope: "node", Serves: []seats.Verb{
|
||||
{Name: "status", Description: "One unit as the service manager sees it now: its states.", Replaces: []string{"systemctl status", "systemctl is-active"}},
|
||||
{Name: "restart", Description: "Restart one unit.", Replaces: []string{"systemctl restart"}},
|
||||
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
|
||||
{Name: "node-hostname", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
|
||||
{Name: "node-hostname", Scope: "node", Serves: []seats.Verb{
|
||||
{Name: "add", Description: "Add one address and its names to the operator's lines of /etc/hosts.", Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}}},
|
||||
}
|
||||
modules := []ModuleTools{{Module: "docker", On: []string{"anchor"}, Replaces: map[string][]string{"docker_logs": {"docker logs"}, "docker_list": {"docker ps"}}},
|
||||
{Module: "systemd", On: []string{"anchor", "laptop"}}}
|
||||
return GuardData{Node: "laptop", Replacements: ReplacementsOf(seats, modules), Machines: []Machine{
|
||||
return GuardData{Node: "laptop", Replacements: ReplacementsOf(defined, modules), Machines: []Machine{
|
||||
{Name: "anchor", Domains: []string{"anchor" + InternalSuffix, "anchor.example"}, Addresses: []string{"192.0.2.10", "10.10.0.1"}},
|
||||
{Name: "laptop", Domains: []string{"laptop" + InternalSuffix}, Addresses: []string{"10.10.0.4"}},
|
||||
}}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// aBus is the states claude-code reads, as a test holds them: by read, key and value.
|
||||
@@ -33,12 +37,12 @@ func (b aBus) Get(read, key string) ([]byte, bool, error) {
|
||||
}
|
||||
|
||||
// someSeats are two seats as the seats' contract says them: one held once for the mesh, one on every machine.
|
||||
func someSeats() []catalogue.Seat {
|
||||
return []catalogue.Seat{
|
||||
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
|
||||
func someSeats() []seats.Seat {
|
||||
return []seats.Seat{
|
||||
{Name: "mesh-placement", Scope: "mesh", Serves: []seats.Verb{
|
||||
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}},
|
||||
{Name: "propose", Description: "Propose a change."}}},
|
||||
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
|
||||
{Name: "node-service-manager", Scope: "node", Serves: []seats.Verb{
|
||||
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot.", Replaces: []string{"systemctl status", "systemctl is-active"}},
|
||||
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
|
||||
}
|
||||
@@ -192,10 +196,44 @@ func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0615 §1: the tools and the placement come from the owners' first views and their views events, never their state.
|
||||
// A reader whose first views are answered from the fixture reads what the state read gave, and a views event changes
|
||||
// what it reads (CAT-CC3).
|
||||
func TestTheMeshIsReadFromTheOwnersFirstViews(t *testing.T) {
|
||||
bus := aMesh(t)
|
||||
r := newMeshViews()
|
||||
for _, name := range []string{ModulesRead, ManifestsRead, PlacementRead} {
|
||||
state := strings.TrimPrefix(name, strings.Split(name, ".")[0]+".")
|
||||
values := map[string]json.RawMessage{}
|
||||
for k, v := range bus[name] {
|
||||
values[k] = v
|
||||
}
|
||||
f := r.Follow(strings.Split(name, ".")[0], state, "x")
|
||||
f.Ask = func(context.Context, string) (views.Answer, error) {
|
||||
return views.Answer{V: views.Version, Owner: f.Owner, State: state, Revision: 5, Created: time.Unix(1, 0), Values: values, At: time.Now()}, nil
|
||||
}
|
||||
if err := f.Start(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
want, err := ReadMeshToolsFrom(bus, seats.List(), resolving)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := ReadMeshToolsFrom(viewStates{r}, seats.List(), resolving)
|
||||
if err != nil || !reflect.DeepEqual(got, want) || len(got.Replacements) == 0 {
|
||||
t.Fatalf("from the views: %+v, %v\nfrom the state: %+v", got, err, want)
|
||||
}
|
||||
// A reader not yet answered says so, and reads nothing.
|
||||
if _, err := ReadMeshToolsFrom(viewStates{newMeshViews()}, seats.List(), resolving); err == nil || !strings.Contains(err.Error(), "no first view") {
|
||||
t.Fatalf("an unanswered first view: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1126 B13 AC1 (ADR 0590): over the seats the mesh defines, the rendered instructions, their table among them,
|
||||
// name no address of the controller, which is not placed: each job is named by the seat that owns it now.
|
||||
func TestTheRenderedInstructionsNameNoControllerAddress(t *testing.T) {
|
||||
m, err := ReadMeshToolsFrom(aMesh(t), catalogue.Seats(), resolving)
|
||||
m, err := ReadMeshToolsFrom(aMesh(t), seats.List(), resolving)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,17 @@ type Judged struct {
|
||||
Pending bool `json:"pending,omitempty"`
|
||||
}
|
||||
|
||||
// JudgedEvent is the body of `judged`: one per judgement asked with a bound past JudgeBound, said when it ends, with
|
||||
// its answer or why there is none. The question and the schema are never in it.
|
||||
type JudgedEvent struct {
|
||||
Ticket string `json:"ticket"`
|
||||
Node string `json:"node"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Seconds float64 `json:"seconds,omitempty"`
|
||||
Answer json.RawMessage `json:"answer,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// running is one judgement asked with a bound past JudgeBound, until it is collected.
|
||||
type running struct {
|
||||
done chan struct{}
|
||||
@@ -92,6 +103,9 @@ type Judge struct {
|
||||
// Wait is how long a call waits for a long judgement before it answers its ticket; JudgeWait unless a test
|
||||
// says less.
|
||||
Wait time.Duration
|
||||
// Say tells the bus a ticketed judgement ended: the event `judged`, once, under the message id it is given so a
|
||||
// repeat is dropped. Nil says nothing (a test's judge); the ticket is collected by claude_code_judged all the same.
|
||||
Say func(body JudgedEvent, id string) error
|
||||
turn chan struct{}
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -208,10 +222,28 @@ func (j *Judge) start(schema, model, question string, bound time.Duration) (*Jud
|
||||
go func() {
|
||||
r.out, r.err = j.run(schema, model, question, bound)
|
||||
close(r.done)
|
||||
j.said(ticket, r)
|
||||
}()
|
||||
return j.Collect(ticket)
|
||||
}
|
||||
|
||||
// said tells the bus that the judgement under a ticket ended (the event `judged`), once. A failure to say is logged: the
|
||||
// ticket stays collectible by claude_code_judged, and the asker's own bound ends its wait.
|
||||
func (j *Judge) said(ticket string, r *running) {
|
||||
if j.Say == nil {
|
||||
return
|
||||
}
|
||||
e := JudgedEvent{Ticket: ticket, Node: j.P.Node}
|
||||
if r.err != nil {
|
||||
e.Error = r.err.Error()
|
||||
} else {
|
||||
e.Model, e.Seconds, e.Answer = r.out.Model, r.out.Seconds, r.out.Answer
|
||||
}
|
||||
if err := j.Say(e, "judged-"+j.P.Node+"-"+ticket); err != nil {
|
||||
say("the judgement under ticket %s ended and could not be said on the bus: %v", ticket, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect waits as long as a call may for the judgement under a ticket: its answer, why it failed, or that it is
|
||||
// still pending. A ticket is answered once; an answer nobody collects is kept JudgedKept past its bound.
|
||||
func (j *Judge) Collect(ticket string) (*Judged, error) {
|
||||
|
||||
@@ -182,6 +182,9 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
|
||||
}
|
||||
})
|
||||
j.Wait = 50 * time.Millisecond
|
||||
said := make(chan JudgedEvent, 4)
|
||||
ids := make(chan string, 4)
|
||||
j.Say = func(e JudgedEvent, id string) error { said <- e; ids <- id; return nil }
|
||||
start := time.Now()
|
||||
out, err := j.Judge(JudgeAsk{Question: "a long record", Schema: verdictSchema, Within: 90 * time.Second})
|
||||
if err != nil {
|
||||
@@ -203,6 +206,21 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
|
||||
if got.Pending || string(got.Answer) != `{"verdict":"none"}` || got.Ticket != out.Ticket {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
// One `judged` per ticket, with the answer, under an id of the ticket's own; none for a judgement with no ticket.
|
||||
select {
|
||||
case e := <-said:
|
||||
if e.Ticket != out.Ticket || e.Node != "control" || string(e.Answer) != `{"verdict":"none"}` || e.Error != "" || e.Model != "sonnet" {
|
||||
t.Fatalf("judged: %+v", e)
|
||||
}
|
||||
if id := <-ids; id != "judged-control-"+out.Ticket {
|
||||
t.Fatalf("judged under %q", id)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the ended judgement was not said")
|
||||
}
|
||||
if len(said) != 0 {
|
||||
t.Fatal("judged said twice")
|
||||
}
|
||||
// Read once the judgement is collected: its end is what orders the agent's write before this read.
|
||||
if left := deadline.Sub(start); left < 80*time.Second || left > 91*time.Second {
|
||||
t.Fatalf("the judgement's bound is %s, not the 90s asked", left)
|
||||
@@ -224,10 +242,23 @@ func TestALongJudgementIsBoundedAndSaysWhyItFailed(t *testing.T) {
|
||||
}
|
||||
j.Wait = 20 * time.Millisecond
|
||||
j.Bound = time.Second // the test's stand-in for JudgeBound: within is past it, so a ticket; far past Wait on a loaded machine
|
||||
failed := make(chan JudgedEvent, 1)
|
||||
j.Say = func(e JudgedEvent, _ string) error { failed <- e; return nil }
|
||||
out, err := j.Judge(JudgeAsk{Question: "q", Schema: verdictSchema, Within: 2 * time.Second})
|
||||
if err != nil || !out.Pending {
|
||||
t.Fatalf("%+v %v", out, err)
|
||||
}
|
||||
defer func() {
|
||||
// A failed judgement is said too, with why and no answer.
|
||||
select {
|
||||
case e := <-failed:
|
||||
if e.Ticket != out.Ticket || e.Answer != nil || !strings.Contains(e.Error, "did not answer within") {
|
||||
t.Errorf("judged: %+v", e)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Error("the failed judgement was not said")
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 3000; i++ {
|
||||
got, err := j.Collect(out.Ticket)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -21,9 +22,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
func say(format string, args ...any) {
|
||||
@@ -180,12 +185,12 @@ func (s memoryStateOf) Delete(key string) error { return s.s.Delete(key) }
|
||||
var memoryCarrier *MemoryCarrier
|
||||
|
||||
// sayCondition says a condition on the operator-channel seat under this module's name (novox/hq ADR 0437 §9).
|
||||
func sayCondition(c asks.Condition) error {
|
||||
subject, err := asks.ConditionSubject("claude-code")
|
||||
func sayCondition(c oc.Condition) error {
|
||||
subject, err := occ.ConditionSubject("claude-code")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := asks.EncodeCondition(c)
|
||||
raw, err := occ.EncodeCondition(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,23 +210,46 @@ func (s stateOf) Get(key string) (json.RawMessage, bool, error) {
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// nodesRunningMe is the nodes claude-code runs on, from the seat mesh-placement's placement — for the register
|
||||
// tool's question.
|
||||
func nodesRunningMe() ([]string, error) { return NodesRunning(busStates{}, "claude-code") }
|
||||
// nodesRunningMe is the nodes claude-code runs on, from the assigner's placement — for the register tool's question.
|
||||
func nodesRunningMe() ([]string, error) { return NodesRunning(viewStates{meshViews}, "claude-code") }
|
||||
|
||||
// busStates reads the states the manifest declares in its reads through the runtime.
|
||||
type busStates struct{}
|
||||
// The owners' read tools of the states this module follows, which the manifest invokes (novox/hq ADR 0615 §1).
|
||||
const (
|
||||
ModulesTool = delivery.IndexerModule + ".indexer_read_modules"
|
||||
ManifestsTool = delivery.IndexerModule + ".indexer_read_manifests"
|
||||
PlacementTool = delivery.AssignerModule + ".assigner_read_placement"
|
||||
)
|
||||
|
||||
func (busStates) Keys(read string) ([]string, error) { return stdio.State(read).Keys() }
|
||||
// meshViews is this module's picture of the indexer's modules and manifests and the assigner's placement: each asked
|
||||
// once of the owner's read tool, then followed on the owner's views family. It reads none of their state.
|
||||
var meshViews *views.Reader
|
||||
|
||||
func (busStates) Get(read, key string) ([]byte, bool, error) {
|
||||
e, err := stdio.State(read).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value, true, nil
|
||||
// meshChanged is told, without waiting, that a followed state changed: the instructions' table is read again.
|
||||
var meshChanged = make(chan struct{}, 1)
|
||||
|
||||
// newMeshViews builds the reader of those three states; Start asks them.
|
||||
func newMeshViews() *views.Reader {
|
||||
v := &views.Reader{Module: Module, Logf: func(f string, a ...any) { say(f, a...) }, Findings: views.OwnFindings{},
|
||||
Acts: handacts.Recorder{Ask: stdio.Ask},
|
||||
Changed: func(string, string, json.RawMessage, bool) {
|
||||
select {
|
||||
case meshChanged <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}}
|
||||
v.Follow(delivery.IndexerModule, "modules", ModulesTool)
|
||||
v.Follow(delivery.IndexerModule, "manifests", ManifestsTool)
|
||||
v.Follow(delivery.AssignerModule, "placement", PlacementTool)
|
||||
return v
|
||||
}
|
||||
|
||||
// viewStates reads the followed states from the reader's pictures, never from the owners' state.
|
||||
type viewStates struct{ v *views.Reader }
|
||||
|
||||
func (s viewStates) Keys(read string) ([]string, error) { return s.v.Keys(read) }
|
||||
|
||||
func (s viewStates) Get(read, key string) ([]byte, bool, error) { return s.v.Get(read, key) }
|
||||
|
||||
func fingerprintOfFile(path string) any {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -291,7 +319,7 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
|
||||
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after reading the bus again for what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
answer := map[string]any{}
|
||||
if m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup); err != nil {
|
||||
if m, err := ReadMeshToolsFrom(viewStates{meshViews}, seats.List(), lookup); err != nil {
|
||||
answer["asked"] = "the bus could not be read in full (" + err.Error() + "); what was kept is rendered"
|
||||
} else {
|
||||
KeepMeshTools(p, m)
|
||||
@@ -372,7 +400,12 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
|
||||
out = append(out, proposalTools(p)...)
|
||||
out = append(out, waitTools(p)...)
|
||||
judge := NewJudge(p)
|
||||
// The event `judged`, one per ticket when its judgement ends: the asker hears it instead of asking again.
|
||||
judge.Say = func(e JudgedEvent, id string) error {
|
||||
return stdio.Emit("judged", e, stdio.EmitOptions{MessageID: id})
|
||||
}
|
||||
out = append(out, judgeTool(judge), judgedTool(judge))
|
||||
out = append(out, meshViews.Tools("claude_code")...)
|
||||
return append(out, configTools(p, config, configView)...)
|
||||
}
|
||||
|
||||
@@ -418,6 +451,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
p, launched := PathsFrom(os.Getenv)
|
||||
meshViews = newMeshViews()
|
||||
if launched {
|
||||
fsys = FilesFor(p)
|
||||
}
|
||||
@@ -447,6 +481,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
meshViews.Start(context.Background())
|
||||
go refreshMeshTools(p)
|
||||
servers := stateOf{stdio.State("servers")}
|
||||
view := NewServerView(p)
|
||||
|
||||
@@ -5,7 +5,7 @@ package main
|
||||
// replaces, and the mesh's machines by every name and address a session could reach one by.
|
||||
//
|
||||
// Read from the bus at start and every few minutes (issue 1126 B13; the controller that answered this before is
|
||||
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-sdk catalogue), the modules'
|
||||
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-contracts), the modules'
|
||||
// own tools from the manifests the indexer keeps (`mesh-delivery-indexer.modules` and `.manifests`), and the
|
||||
// machines and where each module runs from the seat mesh-placement's `placement`. Kept in the module's state so
|
||||
// a render never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -217,14 +217,14 @@ func firstSentence(s string) string {
|
||||
|
||||
// ReplacementsOf is every seat verb and module tool that says it replaces a command: the seats' verbs first, in the
|
||||
// seats' order, then the modules' own tools by module and tool.
|
||||
func ReplacementsOf(seats []catalogue.Seat, modules []ModuleTools) []Replacement {
|
||||
func ReplacementsOf(defined []seats.Seat, modules []ModuleTools) []Replacement {
|
||||
var out []Replacement
|
||||
for _, s := range seats {
|
||||
for _, s := range defined {
|
||||
for _, v := range s.Serves {
|
||||
if len(v.Replaces) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == catalogue.ScopeNode, Seat: true,
|
||||
out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == "node", Seat: true,
|
||||
Does: firstSentence(v.Description), Replaces: v.Replaces})
|
||||
}
|
||||
}
|
||||
@@ -257,15 +257,15 @@ func lookup(name string) []string {
|
||||
|
||||
// ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node placement names
|
||||
// with its domains and addresses. A part that cannot be read is left out and said in the error; what was read is kept.
|
||||
func ReadMeshToolsFrom(st States, seats []catalogue.Seat, resolve func(string) []string) (MeshTools, error) {
|
||||
func ReadMeshToolsFrom(st States, defined []seats.Seat, resolve func(string) []string) (MeshTools, error) {
|
||||
var m MeshTools
|
||||
placed, err := placements(st)
|
||||
if err != nil {
|
||||
m.Replacements = ReplacementsOf(seats, nil)
|
||||
m.Replacements = ReplacementsOf(defined, nil)
|
||||
return m, err
|
||||
}
|
||||
modules, merr := modulesTools(st, runsOn(placed))
|
||||
m.Replacements = ReplacementsOf(seats, modules)
|
||||
m.Replacements = ReplacementsOf(defined, modules)
|
||||
for node, p := range placed {
|
||||
machine := Machine{Name: node, Domains: []string{node + InternalSuffix}}
|
||||
if d := strings.ToLower(strings.TrimSpace(p.Decided.PublicDomain)); d != "" {
|
||||
@@ -334,13 +334,23 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
|
||||
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
|
||||
}
|
||||
|
||||
// MeshToolsEvery is how often the bus is read again.
|
||||
// MeshToolsEvery is how often the machines' addresses are resolved again; the tools and the placement are read again
|
||||
// when their followed states change (a views event), not on this timer.
|
||||
const MeshToolsEvery = 10 * time.Minute
|
||||
|
||||
// refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed.
|
||||
// MeshToolsFirst is how long the first read waits for the first views to be answered.
|
||||
const MeshToolsFirst = 2 * time.Minute
|
||||
|
||||
// refreshMeshTools reads the pictures once the first views are answered, and again whenever one changes (and the
|
||||
// machines' addresses every few minutes), and renders when what it said changed.
|
||||
func refreshMeshTools(p Paths) {
|
||||
select {
|
||||
case <-meshViews.Ready():
|
||||
case <-time.After(MeshToolsFirst):
|
||||
say("the first views of the tools and the placement were not answered in %s; reading what is held", MeshToolsFirst)
|
||||
}
|
||||
for {
|
||||
m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup)
|
||||
m, err := ReadMeshToolsFrom(viewStates{meshViews}, seats.List(), lookup)
|
||||
if err != nil {
|
||||
say("reading what each tool replaces and which machines the mesh has: %v", err)
|
||||
}
|
||||
@@ -351,7 +361,15 @@ func refreshMeshTools(p Paths) {
|
||||
say("the mesh's tools changed: %d replace a command, %d machines; rendered", len(m.Replacements), len(m.Machines))
|
||||
}
|
||||
}
|
||||
time.Sleep(MeshToolsEvery)
|
||||
select {
|
||||
case <-meshChanged:
|
||||
time.Sleep(2 * time.Second) // a burst of events is one read
|
||||
select {
|
||||
case <-meshChanged:
|
||||
default:
|
||||
}
|
||||
case <-time.After(MeshToolsEvery):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// Proposing is what the proposal tools reach: the `proposals` state, the operator channel, the controller's
|
||||
@@ -71,9 +71,13 @@ func (d *Proposing) watch(p Paths) {
|
||||
}
|
||||
}
|
||||
go persist("hearing the operator's answers", func() error {
|
||||
return stdio.Subscribe(asks.Seat+".decided."+Module, func(e stdio.Envelope) error {
|
||||
var w asks.Warrant
|
||||
if json.Unmarshal(e.Body, &w) == nil {
|
||||
return stdio.Subscribe(oc.Seat+".decided."+Module, func(e stdio.Envelope) error {
|
||||
var w oc.Warrant
|
||||
if err := json.Unmarshal(e.Body, &w); err == nil {
|
||||
if _, err := warrantAt(w); err != nil {
|
||||
say("not taking the router's word on %s: %v", w.Ask, err)
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(w.Ask, QuestionPrefix) {
|
||||
HeardQuestionEnd(d.questions(), w)
|
||||
return nil
|
||||
|
||||
@@ -15,7 +15,7 @@ package main
|
||||
// and lets this module read only its own asks);
|
||||
// 2. composes the ask again from the proposal — the words, the options, the expiry, the digest of the text —
|
||||
// and holds the router's warrant to it: the same ask digest, an option it offered, at its level, in time
|
||||
// (asks.Warrant.For), and the option's bound act is this proposal's (asks.Option.Performs). An ask published
|
||||
// (oc.Warrant.For), and the option's bound act is this proposal's (oc.Option.Performs). An ask published
|
||||
// under this module's name with other words, or a proposal changed after it was asked, finds the digests
|
||||
// differ and nothing is done;
|
||||
// 3. applies the approved text to its own file, `approved.json` in the module's state directory, which is what
|
||||
@@ -42,8 +42,9 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// Module is this module's name: the one the bus grants its asks under.
|
||||
@@ -68,7 +69,7 @@ const RouterKeeps = 30 * 24 * time.Hour
|
||||
|
||||
// ProposalLasts is how long the operator has to answer: a day less a margin, so the router never refuses it for
|
||||
// lasting a day and a moment (the SDK's bound for an approval).
|
||||
const ProposalLasts = asks.ApproveLasts - 10*time.Minute
|
||||
const ProposalLasts = occ.ApproveLasts - 10*time.Minute
|
||||
|
||||
// What a proposal does.
|
||||
const (
|
||||
@@ -164,8 +165,8 @@ func joinAnd(list []string) string {
|
||||
|
||||
// Act is what an answer binds: this proposal, whole, and the answer. The router's warrant names the ask's digest,
|
||||
// and the ask names this act's digest in the option, so an approval authorises this text and nothing else.
|
||||
func (p Proposal) Act(option string) asks.Act {
|
||||
return asks.Act{"verb": Module + ".instructions", "option": option, "proposal": p.ID, "action": p.Action,
|
||||
func (p Proposal) Act(option string) occ.Act {
|
||||
return occ.Act{"verb": Module + ".instructions", "option": option, "proposal": p.ID, "action": p.Action,
|
||||
"scope": p.Scope, "nodes": strings.Join(p.Nodes, ","), "name": p.Name, "digest": p.Digest, "from": p.From,
|
||||
"at": p.At.UTC().Format(time.RFC3339Nano)}
|
||||
}
|
||||
@@ -175,7 +176,7 @@ const AskGlance = 1
|
||||
|
||||
// Ask is the ask for a proposal, composed from it alone: every node composes the same one, and holds the
|
||||
// router's warrant to it — in the form it was asked in.
|
||||
func (p Proposal) Ask() asks.Ask {
|
||||
func (p Proposal) Ask() oc.Ask {
|
||||
if p.Form == AskGlance {
|
||||
return p.glanceAsk()
|
||||
}
|
||||
@@ -185,7 +186,7 @@ func (p Proposal) Ask() asks.Ask {
|
||||
// glanceAsk is the ask read at a glance on the phone (novox/hq issue 639): its headline, and as its explanation
|
||||
// the summary of what it does; the text, where it came from, its fingerprint and where to read all of it are its
|
||||
// Details, which the router notes after the answers, collapsed where the channel can.
|
||||
func (p Proposal) glanceAsk() asks.Ask {
|
||||
func (p Proposal) glanceAsk() oc.Ask {
|
||||
verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action]
|
||||
shown, whole := excerpt(p.Text)
|
||||
part := ""
|
||||
@@ -193,7 +194,7 @@ func (p Proposal) glanceAsk() asks.Ask {
|
||||
part = " Part shown"
|
||||
}
|
||||
headline := fmt.Sprintf("%s Claude instructions '%s'?%s", verb, p.Name, part)
|
||||
if len([]rune(headline)) > asks.HeadlineLength {
|
||||
if len([]rune(headline)) > occ.HeadlineLength {
|
||||
headline = verb + " Claude instructions?" + part
|
||||
}
|
||||
var d strings.Builder
|
||||
@@ -214,7 +215,7 @@ func (p Proposal) glanceAsk() asks.Ask {
|
||||
if p.Action == ActionRemove {
|
||||
does = "the instructions are removed from those sessions"
|
||||
}
|
||||
return p.withOptions(asks.Ask{ID: p.ID, Headline: headline, Explanation: p.summary(), Details: d.String()}, does)
|
||||
return p.withOptions(oc.Ask{ID: p.ID, Headline: headline, Explanation: p.summary(), Details: d.String()}, does)
|
||||
}
|
||||
|
||||
// summary is what the proposal does, at a glance: its proposer's words, or the module's.
|
||||
@@ -244,8 +245,8 @@ func SummaryProblem(summary string) string {
|
||||
return ""
|
||||
case strings.Contains(s, "\n"):
|
||||
return "the summary is more than one line"
|
||||
case len([]rune(s)) > asks.SummaryLength:
|
||||
return fmt.Sprintf("the summary is %d characters, more than the %d a phone shows at a glance", len([]rune(s)), asks.SummaryLength)
|
||||
case len([]rune(s)) > occ.SummaryLength:
|
||||
return fmt.Sprintf("the summary is %d characters, more than the %d a phone shows at a glance", len([]rune(s)), occ.SummaryLength)
|
||||
case !shownLine(s):
|
||||
return "the summary names a path, an address, a host name or a long word, which a phone is not shown"
|
||||
}
|
||||
@@ -254,8 +255,8 @@ func SummaryProblem(summary string) string {
|
||||
|
||||
// withOptions is an ask with what every form of it carries: who answers, its expiry, what it is about, and its two
|
||||
// options, each binding its act.
|
||||
func (p Proposal) withOptions(a asks.Ask, does string) asks.Ask {
|
||||
a.Who, a.Expires, a.OnExpiry = asks.Operator, p.At.Add(ProposalLasts), "the proposal is discarded; nothing changes"
|
||||
func (p Proposal) withOptions(a oc.Ask, does string) oc.Ask {
|
||||
a.Who, a.Expires, a.OnExpiry = occ.Operator, occ.FormatTime(p.At.Add(ProposalLasts)), "the proposal is discarded; nothing changes"
|
||||
a.About = "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name
|
||||
if len(p.Nodes) == 0 {
|
||||
a.About = "instructions." + p.Scope + "." + p.Name
|
||||
@@ -264,15 +265,15 @@ func (p Proposal) withOptions(a asks.Ask, does string) asks.Ask {
|
||||
{OptionApprove, "Approve", does},
|
||||
{OptionDecline, "Decline", "nothing changes; the proposal is discarded"},
|
||||
} {
|
||||
binds, _ := asks.ActDigest(p.Act(o.id))
|
||||
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
|
||||
binds, _ := occ.ActDigest(p.Act(o.id))
|
||||
a.Options = append(a.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// listedAsk is the ask as it was composed before issue 639, the text in its explanation: composed only to hold the
|
||||
// warrant of a proposal made then.
|
||||
func (p Proposal) listedAsk() asks.Ask {
|
||||
func (p Proposal) listedAsk() oc.Ask {
|
||||
verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action]
|
||||
shown, whole := excerpt(p.Text)
|
||||
part := ""
|
||||
@@ -280,7 +281,7 @@ func (p Proposal) listedAsk() asks.Ask {
|
||||
part = " Part shown"
|
||||
}
|
||||
headline := fmt.Sprintf("%s Claude instructions '%s'?%s", verb, p.Name, part)
|
||||
if len([]rune(headline)) > asks.HeadlineLength {
|
||||
if len([]rune(headline)) > occ.HeadlineLength {
|
||||
headline = verb + " Claude instructions?" + part
|
||||
}
|
||||
var b strings.Builder
|
||||
@@ -311,8 +312,8 @@ func (p Proposal) listedAsk() asks.Ask {
|
||||
if p.Action == ActionRemove {
|
||||
does = "the instructions are removed from those sessions"
|
||||
}
|
||||
a := asks.Ask{ID: p.ID, Headline: headline, Explanation: strings.TrimRight(b.String(), "\n"), Who: asks.Operator,
|
||||
Expires: p.At.Add(ProposalLasts), OnExpiry: "the proposal is discarded; nothing changes",
|
||||
a := oc.Ask{ID: p.ID, Headline: headline, Explanation: strings.TrimRight(b.String(), "\n"), Who: occ.Operator,
|
||||
Expires: occ.FormatTime(p.At.Add(ProposalLasts)), OnExpiry: "the proposal is discarded; nothing changes",
|
||||
About: "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name}
|
||||
if len(p.Nodes) == 0 {
|
||||
a.About = "instructions." + p.Scope + "." + p.Name
|
||||
@@ -321,8 +322,8 @@ func (p Proposal) listedAsk() asks.Ask {
|
||||
{OptionApprove, "Approve", does},
|
||||
{OptionDecline, "Decline", "nothing changes; the proposal is discarded"},
|
||||
} {
|
||||
binds, _ := asks.ActDigest(p.Act(o.id))
|
||||
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
|
||||
binds, _ := occ.ActDigest(p.Act(o.id))
|
||||
a.Options = append(a.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -433,7 +434,7 @@ func invisible(text string) string {
|
||||
// write the record and ask for it under this module's name.
|
||||
func ProposalProblem(p Proposal) string {
|
||||
switch {
|
||||
case !asks.UsableID(p.ID):
|
||||
case !occ.UsableID(p.ID):
|
||||
return "its id is not one"
|
||||
case !itemName.MatchString(p.Name):
|
||||
return "its name is not one"
|
||||
@@ -838,21 +839,21 @@ func Propose(p Paths, in ProposeInput, store ProposalStore, bus AskBus, now time
|
||||
if why := approverReady(bus); why != "" {
|
||||
return refuse(why + "; " + AtTheTerminalInstead)
|
||||
}
|
||||
if waiting := waitingProposals(store, approved, now); waiting >= asks.MostOpen {
|
||||
if waiting := waitingProposals(store, approved, now); waiting >= occ.MostOpen {
|
||||
return refuse(fmt.Sprintf("%d proposals already wait for the operator's answer, and the operator is asked at most %d "+
|
||||
"questions at once; claude_code_proposals lists them", waiting, asks.MostOpen))
|
||||
"questions at once; claude_code_proposals lists them", waiting, occ.MostOpen))
|
||||
}
|
||||
if problem := ProposalProblem(prop); problem != "" {
|
||||
return refuse(problem)
|
||||
}
|
||||
ask := prop.Ask()
|
||||
if err := ask.Check(now); err != nil {
|
||||
if err := occ.Check(ask, now); err != nil {
|
||||
return refuse(err.Error())
|
||||
}
|
||||
if err := store.Create(prop.ID, prop); err != nil {
|
||||
return nil, fmt.Errorf("the proposal could not be kept, so the operator was not asked: %w", err)
|
||||
}
|
||||
if err := bus.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
|
||||
if err := bus.Publish(oc.AcceptSubjectAsk(Module), ask, ask.ID); err != nil {
|
||||
prop.Asked = "not asked: " + err.Error()
|
||||
_ = store.Put(prop.ID, prop)
|
||||
say("proposal %s (%s %s %s) could not be asked: %v", prop.ID, prop.Action, prop.Scope, prop.Name, err)
|
||||
@@ -874,11 +875,15 @@ func Propose(p Paths, in ProposeInput, store ProposalStore, bus AskBus, now time
|
||||
}
|
||||
time.Sleep(RouterAnswersEvery)
|
||||
}
|
||||
expires, err := askExpires(ask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"router": taken,
|
||||
"proposed": prop.ID, "action": prop.Action, "reaches": prop.where(), "fingerprint": prop.Fingerprint(),
|
||||
"asked": "the operator is asked on a channel that proves who answers: \"" + ask.Headline + "\"",
|
||||
"expires": ask.Expires.Format(time.RFC3339),
|
||||
"expires": expires.Format(time.RFC3339),
|
||||
"until then": "nothing changes on any node; the text becomes part of those sessions only on the operator's Approve, " +
|
||||
"and is discarded on Decline or at expiry. claude_code_proposals shows where it stands",
|
||||
}, nil
|
||||
@@ -927,10 +932,10 @@ var (
|
||||
// read.
|
||||
var (
|
||||
endedMu sync.Mutex
|
||||
ended = map[string]asks.Warrant{}
|
||||
ended = map[string]oc.Warrant{}
|
||||
)
|
||||
|
||||
func endedAsk(id string) (asks.Warrant, bool) {
|
||||
func endedAsk(id string) (oc.Warrant, bool) {
|
||||
endedMu.Lock()
|
||||
defer endedMu.Unlock()
|
||||
w, ok := ended[id]
|
||||
@@ -940,8 +945,8 @@ func endedAsk(id string) (asks.Warrant, bool) {
|
||||
// HeardEnd takes the router's word that one of this module's asks ended without a choice: refused, expired,
|
||||
// cancelled or replaced. The proposal is discarded here at once. Nothing is applied on such a word, so it needs no
|
||||
// proof beyond its subject; a choice is only ever taken from the router's record (Reconcile).
|
||||
func HeardEnd(p Paths, store ProposalStore, w asks.Warrant, now time.Time) (string, error) {
|
||||
if w.Asker != Module || w.Outcome == asks.OutcomeChosen || !asks.UsableID(w.Ask) {
|
||||
func HeardEnd(p Paths, store ProposalStore, w oc.Warrant, now time.Time) (string, error) {
|
||||
if w.Asker != Module || w.Outcome == occ.OutcomeChosen || !occ.UsableID(w.Ask) {
|
||||
return "", nil
|
||||
}
|
||||
reconcileMu.Lock()
|
||||
@@ -968,7 +973,7 @@ func HeardEnd(p Paths, store ProposalStore, w asks.Warrant, now time.Time) (stri
|
||||
}
|
||||
// Heard, not read from a record: Reconcile still reads the router's record of it, so an ask the router took after
|
||||
// all (its queue offered it again once a channel was ready) is answered as any other.
|
||||
d := ProposalDecision{At: now.UTC(), Says: w.Says(), Outcome: "discarded: the ask " + string(w.Outcome), Heard: true}
|
||||
d := ProposalDecision{At: now.UTC(), Says: occ.Says(w), Outcome: "discarded: the ask " + string(w.Outcome), Heard: true}
|
||||
if w.Words != "" {
|
||||
d.Outcome += " (" + w.Words + ")"
|
||||
}
|
||||
@@ -986,9 +991,9 @@ func HeardEnd(p Paths, store ProposalStore, w asks.Warrant, now time.Time) (stri
|
||||
|
||||
// routerRecord is the router's record of one ask, as far as this module reads it.
|
||||
type routerRecord struct {
|
||||
State string `json:"state"`
|
||||
Ask *asks.Ask `json:"ask"`
|
||||
Warrant *asks.Warrant `json:"warrant"`
|
||||
State string `json:"state"`
|
||||
Ask *oc.Ask `json:"ask"`
|
||||
Warrant *oc.Warrant `json:"warrant"`
|
||||
}
|
||||
|
||||
var reconcileMu sync.Mutex
|
||||
@@ -1010,7 +1015,7 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
|
||||
}
|
||||
type closed struct {
|
||||
prop Proposal
|
||||
w asks.Warrant
|
||||
w oc.Warrant
|
||||
}
|
||||
var ready []closed
|
||||
discarded := false
|
||||
@@ -1027,8 +1032,8 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
|
||||
if done {
|
||||
// Discarded on the router's word alone: only a record of a person's choice reopens it.
|
||||
var rec routerRecord
|
||||
if err == nil && entry != nil && json.Unmarshal(entry.Value, &rec) == nil && rec.Warrant != nil &&
|
||||
rec.Warrant.Outcome == asks.OutcomeChosen {
|
||||
if err == nil && entry != nil && json.Unmarshal(entry.Value, &rec) == nil && rec.check() == nil && rec.Warrant != nil &&
|
||||
rec.Warrant.Outcome == occ.OutcomeChosen {
|
||||
ready = append(ready, closed{prop, *rec.Warrant})
|
||||
}
|
||||
continue
|
||||
@@ -1046,12 +1051,16 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
|
||||
continue // open, not yet recorded, or not readable now: asked again on the next look
|
||||
}
|
||||
var rec routerRecord
|
||||
if json.Unmarshal(entry.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
|
||||
if json.Unmarshal(entry.Value, &rec) != nil || rec.check() != nil || rec.State == "open" || rec.Warrant == nil {
|
||||
continue
|
||||
}
|
||||
ready = append(ready, closed{prop, *rec.Warrant})
|
||||
}
|
||||
sort.SliceStable(ready, func(i, j int) bool { return ready[i].w.At.Before(ready[j].w.At) })
|
||||
sort.SliceStable(ready, func(i, j int) bool {
|
||||
a, _ := warrantAt(ready[i].w) // checked when read
|
||||
b, _ := warrantAt(ready[j].w)
|
||||
return a.Before(b)
|
||||
})
|
||||
changed, wrote := false, discarded
|
||||
var said []string
|
||||
for _, c := range ready {
|
||||
@@ -1108,17 +1117,21 @@ func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now ti
|
||||
|
||||
// decide holds one closed ask's warrant to its proposal, and applies it to this node's record when it holds. final
|
||||
// is false when the answer did not hold here: it is judged again on the next look, and nothing is applied.
|
||||
func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time.Time) (ProposalDecision, bool, string, bool) {
|
||||
d := ProposalDecision{At: now.UTC(), Says: w.Says()}
|
||||
func decide(p Paths, approved *Approved, prop Proposal, w oc.Warrant, now time.Time) (ProposalDecision, bool, string, bool) {
|
||||
d := ProposalDecision{At: now.UTC(), Says: occ.Says(w)}
|
||||
refused := func(why string) (ProposalDecision, bool, string, bool) {
|
||||
d.Outcome = "refused: " + why
|
||||
say("REFUSED the answer to proposal %s: %s", prop.ID, why)
|
||||
return d, false, "proposal " + prop.ID + ": refused, " + why, false
|
||||
}
|
||||
at, err := warrantAt(w)
|
||||
if err != nil {
|
||||
return refused(err.Error())
|
||||
}
|
||||
if w.Asker != Module || w.Ask != prop.ID {
|
||||
return refused(fmt.Sprintf("the router's record is for %s's ask %s", w.Asker, w.Ask))
|
||||
}
|
||||
if w.Outcome != asks.OutcomeChosen {
|
||||
if w.Outcome != occ.OutcomeChosen {
|
||||
d.Outcome = "discarded: the ask " + string(w.Outcome)
|
||||
if w.Words != "" {
|
||||
d.Outcome += " (" + w.Words + ")"
|
||||
@@ -1130,11 +1143,11 @@ func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time
|
||||
if problem := ProposalProblem(prop); problem != "" {
|
||||
return refused("the proposal is not one this module makes: " + problem)
|
||||
}
|
||||
option, err := w.For(Module, prop.Ask())
|
||||
option, err := occ.For(w, Module, prop.Ask())
|
||||
if err != nil {
|
||||
return refused(err.Error())
|
||||
}
|
||||
if err := option.Performs(prop.Act(option.ID)); err != nil {
|
||||
if err := occ.Performs(option, prop.Act(option.ID)); err != nil {
|
||||
return refused(err.Error())
|
||||
}
|
||||
d.Warranted = true
|
||||
@@ -1146,7 +1159,7 @@ func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time
|
||||
return refused("the option " + option.ID + " is not one a proposal offers")
|
||||
}
|
||||
key := sectionKey(prop.Scope, prop.Nodes, prop.Name)
|
||||
if last, ok := approved.Changed[key]; ok && !w.At.After(last) {
|
||||
if last, ok := approved.Changed[key]; ok && !at.After(last) {
|
||||
d.Outcome = "approved, and superseded here: a change of this section approved later was applied first"
|
||||
return d, false, "proposal " + prop.ID + ": " + d.Outcome, true
|
||||
}
|
||||
@@ -1157,16 +1170,16 @@ func decide(p Paths, approved *Approved, prop Proposal, w asks.Warrant, now time
|
||||
held, ok := approved.Sections[key]
|
||||
if !ok || held.Digest != prop.Digest {
|
||||
d.Outcome = "approved, nothing to remove here: this node holds no section " + prop.Name + " with that fingerprint"
|
||||
approved.Changed[key] = w.At.UTC()
|
||||
approved.Changed[key] = at.UTC()
|
||||
return d, false, "proposal " + prop.ID + ": " + d.Outcome, true
|
||||
}
|
||||
delete(approved.Sections, key)
|
||||
default:
|
||||
approved.Sections[key] = ApprovedSection{Name: prop.Name, Scope: prop.Scope, Nodes: prop.Nodes, Text: prop.Text,
|
||||
Digest: prop.Digest, Proposal: prop.ID, From: prop.From, Approved: w.At.UTC(), By: w.Says()}
|
||||
Digest: prop.Digest, Proposal: prop.ID, From: prop.From, Approved: at.UTC(), By: occ.Says(w)}
|
||||
}
|
||||
approved.Changed[key] = w.At.UTC()
|
||||
say("proposal %s (%s %s %s, fingerprint %s): %s", prop.ID, prop.Action, prop.Scope, prop.Name, prop.Fingerprint(), w.Says())
|
||||
approved.Changed[key] = at.UTC()
|
||||
say("proposal %s (%s %s %s, fingerprint %s): %s", prop.ID, prop.Action, prop.Scope, prop.Name, prop.Fingerprint(), occ.Says(w))
|
||||
return d, reaches, "proposal " + prop.ID + ": approved, " + prop.Action + " " + prop.Name, true
|
||||
}
|
||||
|
||||
@@ -1222,12 +1235,12 @@ func ListProposals(p Paths, store ProposalStore, bus AskBus, id string, now time
|
||||
// the router took, both read now.
|
||||
if entry, err := bus.Record(prop.ID); err == nil && entry != nil {
|
||||
var rec routerRecord
|
||||
if json.Unmarshal(entry.Value, &rec) == nil {
|
||||
if json.Unmarshal(entry.Value, &rec) == nil && rec.check() == nil {
|
||||
row["router"] = rec.State
|
||||
switch {
|
||||
case rec.Ask == nil:
|
||||
row["the phone"] = "cannot be compared: the router's record keeps no ask"
|
||||
case rec.Ask.Digest() == prop.Ask().Digest():
|
||||
case occ.Digest(*rec.Ask) == occ.Digest(prop.Ask()):
|
||||
row["the phone"] = "shows the ask composed from exactly this text (fingerprint " + prop.Fingerprint() + ")"
|
||||
default:
|
||||
row["the phone"] = "shows ANOTHER ask than this record composes: do not approve it"
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// memProposals is the `proposals` state, shared by every node of a test as the bus shares it.
|
||||
@@ -46,7 +47,7 @@ func (s *memProposals) Get(key string) (json.RawMessage, bool, error) {
|
||||
// router is the operator channel's holder as the module reaches it: what was asked, its record of each ask, and
|
||||
// the channel kinds.
|
||||
type router struct {
|
||||
published map[string]asks.Ask
|
||||
published map[string]oc.Ask
|
||||
subjects []string
|
||||
records map[string]json.RawMessage
|
||||
kinds []stdio.KindHeld
|
||||
@@ -54,7 +55,7 @@ type router struct {
|
||||
}
|
||||
|
||||
func newRouter() *router {
|
||||
return &router{published: map[string]asks.Ask{}, records: map[string]json.RawMessage{},
|
||||
return &router{published: map[string]oc.Ask{}, records: map[string]json.RawMessage{},
|
||||
kinds: []stdio.KindHeld{{Seat: "channel", Kind: "telegram", Module: "telegram", Node: "novox",
|
||||
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
|
||||
}
|
||||
@@ -65,7 +66,7 @@ func (r *router) Publish(subject string, body any, id string) error {
|
||||
}
|
||||
r.subjects = append(r.subjects, subject)
|
||||
raw, _ := json.Marshal(body)
|
||||
var a asks.Ask
|
||||
var a oc.Ask
|
||||
_ = json.Unmarshal(raw, &a)
|
||||
r.published[a.ID] = a
|
||||
r.records[a.ID], _ = json.Marshal(map[string]any{"state": "open"})
|
||||
@@ -81,19 +82,19 @@ func (r *router) Record(id string) (*stdio.StateEntry, error) {
|
||||
func (r *router) Kinds() ([]stdio.KindHeld, error) { return r.kinds, nil }
|
||||
|
||||
// close ends an ask with a warrant, as the router records it.
|
||||
func (r *router) close(w asks.Warrant) {
|
||||
func (r *router) close(w oc.Warrant) {
|
||||
w.Asker = Module
|
||||
state := string(w.Outcome)
|
||||
r.records[w.Ask], _ = json.Marshal(map[string]any{"state": state, "warrant": w})
|
||||
}
|
||||
|
||||
// chose is the warrant for a person's choice of an option of the ask as the router took it.
|
||||
func (r *router) chose(id, option string, at time.Time) asks.Warrant {
|
||||
func (r *router) chose(id, option string, at time.Time) oc.Warrant {
|
||||
a := r.published[id]
|
||||
o, _ := a.Option(option)
|
||||
return asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label, Level: o.Level,
|
||||
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"},
|
||||
Channel: "telegram", Proofs: []string{"P1"}, At: at, AskDigest: a.Digest()}
|
||||
o, _ := occ.OptionOf(a, option)
|
||||
return oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeChosen, Option: o.ID, Label: o.Label, Level: o.Level,
|
||||
By: &oc.Person{Who: occ.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"},
|
||||
Channel: "telegram", Proofs: []string{"P1"}, At: occ.FormatTime(at), AskDigest: occ.Digest(a)}
|
||||
}
|
||||
|
||||
type recorded struct{ asks []string }
|
||||
@@ -144,13 +145,13 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
|
||||
t.Fatalf("the ask was published on %v, not under this module's own name", r.subjects)
|
||||
}
|
||||
a := r.published[id]
|
||||
if err := a.Check(t0); err != nil {
|
||||
if err := occ.Check(a, t0); err != nil {
|
||||
t.Fatalf("the router would refuse the ask: %v", err)
|
||||
}
|
||||
// At a glance (issue 639): what it does is the explanation, one line; the text is the Details, which the
|
||||
// router notes collapsed after the answers.
|
||||
if a.Explanation != "Every Claude session on the mesh reads these 4 lines as the mesh's own word, from its next start until they are removed the same way." ||
|
||||
asks.Glance(a.Explanation) != nil {
|
||||
occ.Glance(a.Explanation) != nil {
|
||||
t.Errorf("the phone does not say what is asked at a glance: %q", a.Explanation)
|
||||
}
|
||||
if a.Headline != "Add Claude instructions 'handoff-from-g14'?" {
|
||||
@@ -163,11 +164,11 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
|
||||
t.Fatalf("options: %+v", a.Options)
|
||||
}
|
||||
for _, o := range a.Options {
|
||||
if o.Level != asks.Approve || !strings.HasPrefix(o.Binds, "sha256:") {
|
||||
if o.Level != occ.Approve || !strings.HasPrefix(o.Binds, "sha256:") {
|
||||
t.Errorf("the option %s is %s binding %q: both answers need a proven sender and bind the act", o.ID, o.Level, o.Binds)
|
||||
}
|
||||
}
|
||||
if !a.Expires.Equal(t0.Add(ProposalLasts)) {
|
||||
if a.Expires != occ.FormatTime(t0.Add(ProposalLasts)) {
|
||||
t.Errorf("expires %s", a.Expires)
|
||||
}
|
||||
// Nothing is active, here or anywhere, while the ask is open.
|
||||
@@ -212,10 +213,10 @@ func TestNothingBecomesActiveWithoutAValidWarrant(t *testing.T) {
|
||||
}{
|
||||
{"declined", func(r *router, _ *memProposals, id string) { r.close(r.chose(id, OptionDecline, t0.Add(time.Hour))) }, "declined"},
|
||||
{"expired", func(r *router, _ *memProposals, id string) {
|
||||
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeExpired, At: t0.Add(24 * time.Hour)})
|
||||
r.close(oc.Warrant{Ask: id, Outcome: occ.OutcomeExpired, At: occ.FormatTime(t0.Add(24 * time.Hour))})
|
||||
}, "discarded"},
|
||||
{"refused by the router", func(r *router, _ *memProposals, id string) {
|
||||
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeRefused, Words: "no channel can carry it", At: t0})
|
||||
r.close(oc.Warrant{Ask: id, Outcome: occ.OutcomeRefused, Words: "no channel can carry it", At: occ.FormatTime(t0)})
|
||||
}, "discarded"},
|
||||
{"the text changed after the ask, with its digest", func(r *router, store *memProposals, id string) {
|
||||
var prop Proposal
|
||||
@@ -243,7 +244,7 @@ func TestNothingBecomesActiveWithoutAValidWarrant(t *testing.T) {
|
||||
}, "refused"},
|
||||
{"answered at the level acknowledge", func(r *router, _ *memProposals, id string) {
|
||||
w := r.chose(id, OptionApprove, t0.Add(time.Hour))
|
||||
w.Level = asks.Acknowledge
|
||||
w.Level = occ.Acknowledge
|
||||
r.close(w)
|
||||
}, "refused"},
|
||||
{"no person chose", func(r *router, _ *memProposals, id string) {
|
||||
@@ -428,7 +429,7 @@ func TestALongTextIsShownInPartAndReadWholeAtTheTerminal(t *testing.T) {
|
||||
!strings.Contains(a.Details, "fingerprint "+out["fingerprint"].(string)) {
|
||||
t.Errorf("the phone does not say where to read all of it: %q", a.Details)
|
||||
}
|
||||
if len(a.Details) > 3000 || asks.Glance(a.Explanation) != nil {
|
||||
if len(a.Details) > 3000 || occ.Glance(a.Explanation) != nil {
|
||||
t.Errorf("the details are %d bytes; the explanation %q", len(a.Details), a.Explanation)
|
||||
}
|
||||
list, err := ListProposals(p, store, r, id, t0)
|
||||
@@ -496,7 +497,7 @@ func TestATextChangedPastWhatThePhoneShowsIsRefused(t *testing.T) {
|
||||
_ = json.Unmarshal(store.m[id], &prop)
|
||||
prop.Text = strings.Replace(prop.Text, "Ask before you push.", "Push without asking.", 1)
|
||||
_ = store.Put(id, prop)
|
||||
if prop.Ask().Digest() != r.published[id].Digest() {
|
||||
if occ.Digest(prop.Ask()) != occ.Digest(r.published[id]) {
|
||||
t.Fatal("the test means a change the ask's words do not show")
|
||||
}
|
||||
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
|
||||
@@ -559,7 +560,7 @@ func TestAForgedRecordIsNotAppliedEvenWhenApproved(t *testing.T) {
|
||||
} {
|
||||
prop.Digest = textDigest(prop.Text)
|
||||
_ = store.Put(prop.ID, prop)
|
||||
if err := r.Publish(asks.AskSubject(Module), prop.Ask(), prop.ID); err != nil {
|
||||
if err := r.Publish(oc.AcceptSubjectAsk(Module), prop.Ask(), prop.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.close(r.chose(prop.ID, OptionApprove, t0.Add(time.Hour)))
|
||||
@@ -718,8 +719,8 @@ func (r *refusingRouter) Publish(subject string, body any, id string) error {
|
||||
r.id = id
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
_, _ = HeardEnd(r.p, r.store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused,
|
||||
Words: "no channel can carry any of its answers now: telegram is not linked", At: t0}, t0)
|
||||
_, _ = HeardEnd(r.p, r.store, oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeRefused,
|
||||
Words: "no channel can carry any of its answers now: telegram is not linked", At: occ.FormatTime(t0)}, t0)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -729,12 +730,23 @@ func TestAnAskTakenAfterItsRefusalIsStillAnswered(t *testing.T) {
|
||||
p, _ := node(t, "novox")
|
||||
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
|
||||
id := proposeHandover(t, p, store, r)
|
||||
if _, err := HeardEnd(p, store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused, Words: "no channel", At: t0}, t0); err != nil {
|
||||
if _, err := HeardEnd(p, store, oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeRefused, Words: "no channel", At: occ.FormatTime(t0)}, t0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := outcomeHere(t, p, id); !strings.HasPrefix(got, "discarded") {
|
||||
t.Fatalf("decided %q", got)
|
||||
}
|
||||
// A warrant whose time does not parse is refused, naming the field, as the decode of it was.
|
||||
bad := r.chose(id, OptionApprove, t0.Add(time.Hour))
|
||||
bad.At = "yesterday"
|
||||
prop, _ := readProposal(store, id)
|
||||
appr := Approved{Decided: map[string]ProposalDecision{}, Changed: map[string]time.Time{}, Refused: map[string]string{}}
|
||||
if d, _, _, _ := decide(p, &appr, prop, bad, t0.Add(time.Hour)); !strings.HasPrefix(d.Outcome, "refused: the warrant's at") {
|
||||
t.Errorf("a warrant with a malformed at was decided %q", d.Outcome)
|
||||
}
|
||||
if (routerRecord{Warrant: &bad}).check() == nil {
|
||||
t.Error("a router record with a malformed at passed its check")
|
||||
}
|
||||
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
|
||||
if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(time.Hour)); !changed {
|
||||
t.Fatal("the approval of an ask taken after its refusal was lost")
|
||||
@@ -769,7 +781,7 @@ func TestAProposalIsAskedAtAGlanceAndAnOlderOneAsItWasAsked(t *testing.T) {
|
||||
older := prop
|
||||
older.Form = 0
|
||||
listed := older.Ask()
|
||||
if !strings.Contains(listed.Explanation, "The whole text:") || listed.Details != "" || listed.Digest() == prop.Ask().Digest() {
|
||||
if !strings.Contains(listed.Explanation, "The whole text:") || listed.Details != "" || occ.Digest(listed) == occ.Digest(prop.Ask()) {
|
||||
t.Errorf("a proposal made before is not composed as it was asked: %q", listed.Explanation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ package main
|
||||
//
|
||||
// The session reads the answer by the question's key with claude_code_answer: from the router's own record of the
|
||||
// ask, which only the router writes and only this module reads, held to the question as the `questions` state keeps
|
||||
// it (asks.Warrant.For and asks.Option.Performs). A record that does not hold is said as not proven, never as a
|
||||
// it (oc.Warrant.For and oc.Option.Performs). A record that does not hold is said as not proven, never as a
|
||||
// choice. An answer informs the session; it authorises nothing (ADR 0286 stays the way to an approved call).
|
||||
|
||||
import (
|
||||
@@ -27,8 +27,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// QuestionsState is the module's state the questions are kept in.
|
||||
@@ -38,16 +39,16 @@ const QuestionsState = "questions"
|
||||
const QuestionPrefix = "q-"
|
||||
|
||||
// MostQuestions is how many questions may wait at once, mesh-wide: the router allows an asker three open asks
|
||||
// (asks.MostOpen), and questions alone never fill them, so one is left for a proposed instruction section (ADR 0274)
|
||||
// (occ.MostOpen), and questions alone never fill them, so one is left for a proposed instruction section (ADR 0274)
|
||||
// while two questions are open. Each open question holds one slot key in the `questions` state (slotKey), taken by
|
||||
// create or by compare-and-set, so two nodes asking at once cannot both take the last one.
|
||||
const MostQuestions = asks.MostOpen - 1
|
||||
const MostQuestions = occ.MostOpen - 1
|
||||
|
||||
// A question the router holds no record of, and whose refusal was not heard here, keeps its slot until it expires:
|
||||
// the router may still have it in its queue (down, or backlogged), and neither the queue's order nor a cancel can
|
||||
// take it back — the queue offers a piece of work again after an error, so a cancel may be taken before the ask it
|
||||
// cancels, and a cancel of an ask not yet recorded is dropped. Once the question has expired the router refuses it
|
||||
// when it is taken (asks.Ask.Check), so the slot is free and the question has ended.
|
||||
// when it is taken (oc.Ask.Check), so the slot is free and the question has ended.
|
||||
|
||||
// slotKey is the key of one of the MostQuestions slots: not a question's key, so no question is ever read from it.
|
||||
func slotKey(n int) string { return fmt.Sprintf("slot-%d", n) }
|
||||
@@ -69,7 +70,7 @@ const (
|
||||
// The bounds of what a question says.
|
||||
const (
|
||||
FewestQuestionOptions = 2
|
||||
MostContextBytes = asks.SummaryLength
|
||||
MostContextBytes = occ.SummaryLength
|
||||
MostMeansBytes = 200
|
||||
MostWhyBytes = 120
|
||||
MostOnExpiryBytes = 200
|
||||
@@ -151,8 +152,8 @@ func (q Question) recommended() (QuestionOption, bool) {
|
||||
}
|
||||
|
||||
// Act is what an answer binds: this question, whole, and the option chosen.
|
||||
func (q Question) Act(option string) asks.Act {
|
||||
act := asks.Act{"verb": Module + ".question", "option": option, "question": q.ID, "headline": q.Headline,
|
||||
func (q Question) Act(option string) occ.Act {
|
||||
act := occ.Act{"verb": Module + ".question", "option": option, "question": q.ID, "headline": q.Headline,
|
||||
"context": q.Context, "why": q.Why, "on-expiry": q.OnExpiry, "by": q.By, "from": q.From,
|
||||
"at": q.At.UTC().Format(time.RFC3339Nano), "expires": q.Expires.UTC().Format(time.RFC3339Nano),
|
||||
"options": strconv.Itoa(len(q.Options))}
|
||||
@@ -163,7 +164,7 @@ func (q Question) Act(option string) asks.Act {
|
||||
}
|
||||
|
||||
// Ask is the ask for a question, composed from it alone, in the form it was asked in.
|
||||
func (q Question) Ask() asks.Ask {
|
||||
func (q Question) Ask() oc.Ask {
|
||||
if q.Form == AskGlance {
|
||||
return q.glanceAsk()
|
||||
}
|
||||
@@ -172,31 +173,31 @@ func (q Question) Ask() asks.Ask {
|
||||
|
||||
// glanceAsk is the question read at a glance on the phone (novox/hq issue 639): its context, then the recommended
|
||||
// answer and why as its one point; who asked and what the answer does are its Details.
|
||||
func (q Question) glanceAsk() asks.Ask {
|
||||
func (q Question) glanceAsk() oc.Ask {
|
||||
explanation := strings.TrimSpace(q.Context)
|
||||
if rec, ok := q.recommended(); ok {
|
||||
explanation += fmt.Sprintf("\n• Recommended: %s, because %s.", rec.Label, strings.TrimRight(strings.TrimSpace(q.Why), "."))
|
||||
}
|
||||
details := fmt.Sprintf("Asked by the agent session %s on %s through the claude-code module, at %s UTC. Your answer is told "+
|
||||
"to that session; it performs nothing by itself.", q.By, q.From, q.At.UTC().Format("15:04 on 2 Jan"))
|
||||
return q.withOptions(asks.Ask{ID: q.ID, Headline: q.Headline, Explanation: explanation, Details: details})
|
||||
return q.withOptions(oc.Ask{ID: q.ID, Headline: q.Headline, Explanation: explanation, Details: details})
|
||||
}
|
||||
|
||||
func (q Question) withOptions(a asks.Ask) asks.Ask {
|
||||
a.Who, a.Expires, a.OnExpiry = asks.Operator, q.Expires, q.OnExpiry
|
||||
func (q Question) withOptions(a oc.Ask) oc.Ask {
|
||||
a.Who, a.Expires, a.OnExpiry = occ.Operator, occ.FormatTime(q.Expires), q.OnExpiry
|
||||
for _, o := range q.Options {
|
||||
does := o.Means
|
||||
if o.Recommended {
|
||||
does = "Recommended. " + does
|
||||
}
|
||||
binds, _ := asks.ActDigest(q.Act(o.ID))
|
||||
a.Options = append(a.Options, asks.Option{ID: o.ID, Label: o.Label, Does: does, Level: asks.Approve, Binds: binds})
|
||||
binds, _ := occ.ActDigest(q.Act(o.ID))
|
||||
a.Options = append(a.Options, oc.Option{ID: o.ID, Label: o.Label, Does: does, Level: occ.Approve, Binds: binds})
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// listedAsk is the ask as it was composed before issue 639: composed only to hold the answer to a question asked then.
|
||||
func (q Question) listedAsk() asks.Ask {
|
||||
func (q Question) listedAsk() oc.Ask {
|
||||
var b strings.Builder
|
||||
b.WriteString(strings.TrimSpace(q.Context))
|
||||
if rec, ok := q.recommended(); ok {
|
||||
@@ -205,15 +206,15 @@ func (q Question) listedAsk() asks.Ask {
|
||||
}
|
||||
fmt.Fprintf(&b, "\n\nAsked by the agent session %s on %s through the claude-code module, at %s UTC. "+
|
||||
"Your answer is told to that session; it performs nothing by itself.", q.By, q.From, q.At.UTC().Format("15:04 on 2 Jan"))
|
||||
a := asks.Ask{ID: q.ID, Headline: q.Headline, Explanation: b.String(), Who: asks.Operator, Expires: q.Expires,
|
||||
a := oc.Ask{ID: q.ID, Headline: q.Headline, Explanation: b.String(), Who: occ.Operator, Expires: occ.FormatTime(q.Expires),
|
||||
OnExpiry: q.OnExpiry}
|
||||
for _, o := range q.Options {
|
||||
does := o.Means
|
||||
if o.Recommended {
|
||||
does = "Recommended. " + does
|
||||
}
|
||||
binds, _ := asks.ActDigest(q.Act(o.ID))
|
||||
a.Options = append(a.Options, asks.Option{ID: o.ID, Label: o.Label, Does: does, Level: asks.Approve, Binds: binds})
|
||||
binds, _ := occ.ActDigest(q.Act(o.ID))
|
||||
a.Options = append(a.Options, oc.Option{ID: o.ID, Label: o.Label, Does: does, Level: occ.Approve, Binds: binds})
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -228,8 +229,8 @@ func QuestionProblem(q Question) string {
|
||||
switch {
|
||||
case strings.TrimSpace(q.Headline) == "":
|
||||
add("it has no headline")
|
||||
case len([]rune(q.Headline)) > asks.HeadlineLength:
|
||||
add("its headline is %d characters, more than %d", len([]rune(q.Headline)), asks.HeadlineLength)
|
||||
case len([]rune(q.Headline)) > occ.HeadlineLength:
|
||||
add("its headline is %d characters, more than %d", len([]rune(q.Headline)), occ.HeadlineLength)
|
||||
}
|
||||
switch {
|
||||
case strings.TrimSpace(q.Context) == "":
|
||||
@@ -239,16 +240,16 @@ func QuestionProblem(q Question) string {
|
||||
case strings.Contains(strings.TrimSpace(q.Context), "\n"):
|
||||
add("its context is more than one line: a phone shows it at a glance as one short paragraph")
|
||||
}
|
||||
if n := len(q.Options); n < FewestQuestionOptions || n > asks.MostOptions {
|
||||
add("it offers %d options; a question offers %d to %d", n, FewestQuestionOptions, asks.MostOptions)
|
||||
if n := len(q.Options); n < FewestQuestionOptions || n > occ.MostOptions {
|
||||
add("it offers %d options; a question offers %d to %d", n, FewestQuestionOptions, occ.MostOptions)
|
||||
}
|
||||
recommended, labels := 0, map[string]bool{}
|
||||
for i, o := range q.Options {
|
||||
switch {
|
||||
case strings.TrimSpace(o.Label) == "":
|
||||
add("option %d has no label", i+1)
|
||||
case len([]rune(o.Label)) > asks.LabelLength:
|
||||
add("option %d's label is longer than %d characters", i+1, asks.LabelLength)
|
||||
case len([]rune(o.Label)) > occ.LabelLength:
|
||||
add("option %d's label is longer than %d characters", i+1, occ.LabelLength)
|
||||
case labels[strings.ToLower(o.Label)]:
|
||||
add("two options are labelled %q", o.Label)
|
||||
}
|
||||
@@ -258,7 +259,7 @@ func QuestionProblem(q Question) string {
|
||||
} else if len(o.Means) > MostMeansBytes {
|
||||
add("what option %d means is longer than %d bytes", i+1, MostMeansBytes)
|
||||
}
|
||||
if !asks.UsableID(o.ID) {
|
||||
if !occ.UsableID(o.ID) {
|
||||
add("option %d's id is not one", i+1)
|
||||
}
|
||||
if o.Recommended {
|
||||
@@ -368,7 +369,7 @@ func AskQuestion(p Paths, in QuestionInput, store QuestionStore, bus AskBus, now
|
||||
return refuse(problem)
|
||||
}
|
||||
ask := q.Ask()
|
||||
if err := ask.Check(now); err != nil {
|
||||
if err := occ.Check(ask, now); err != nil {
|
||||
return refuse(err.Error())
|
||||
}
|
||||
// The question is kept before its slot is taken, so a slot always names a question another node can judge.
|
||||
@@ -384,7 +385,7 @@ func AskQuestion(p Paths, in QuestionInput, store QuestionStore, bus AskBus, now
|
||||
"at once, so one of the router's three open asks is left for a proposed instruction section; read them with "+
|
||||
"claude_code_answer, or wait for one to end", len(open), strings.Join(open, ", "), MostQuestions))
|
||||
}
|
||||
if err := bus.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
|
||||
if err := bus.Publish(oc.AcceptSubjectAsk(Module), ask, ask.ID); err != nil {
|
||||
_ = store.Put(q.ID, tombstone(q, "not asked: "+err.Error()))
|
||||
return nil, fmt.Errorf("the operator could not be asked: %v", err)
|
||||
}
|
||||
@@ -517,10 +518,10 @@ func readQuestionErr(store QuestionStore, key string) (Question, bool, error) {
|
||||
// The router's word that a question ended without a choice, as heard on the decided subject.
|
||||
var (
|
||||
questionEndsMu sync.Mutex
|
||||
questionEnds = map[string]asks.Warrant{}
|
||||
questionEnds = map[string]oc.Warrant{}
|
||||
)
|
||||
|
||||
func questionEnd(id string) (asks.Warrant, bool) {
|
||||
func questionEnd(id string) (oc.Warrant, bool) {
|
||||
questionEndsMu.Lock()
|
||||
defer questionEndsMu.Unlock()
|
||||
w, ok := questionEnds[id]
|
||||
@@ -537,8 +538,8 @@ func forgetQuestionEnd(id string) {
|
||||
// replaced — and keeps it on the question, because a refused ask leaves no record of the router's. A refused question
|
||||
// keeps none of its words (tombstone). A choice is never taken from this word: only from the router's record, held to
|
||||
// the question (ReadAnswer).
|
||||
func HeardQuestionEnd(store QuestionStore, w asks.Warrant) {
|
||||
if w.Asker != Module || w.Outcome == asks.OutcomeChosen || !strings.HasPrefix(w.Ask, QuestionPrefix) {
|
||||
func HeardQuestionEnd(store QuestionStore, w oc.Warrant) {
|
||||
if w.Asker != Module || w.Outcome == occ.OutcomeChosen || !strings.HasPrefix(w.Ask, QuestionPrefix) {
|
||||
return
|
||||
}
|
||||
q, known := readQuestion(store, w.Ask)
|
||||
@@ -550,7 +551,7 @@ func HeardQuestionEnd(store QuestionStore, w asks.Warrant) {
|
||||
if w.Words != "" {
|
||||
ended += ": " + w.Words
|
||||
}
|
||||
if w.Outcome == asks.OutcomeRefused {
|
||||
if w.Outcome == occ.OutcomeRefused {
|
||||
q = tombstone(q, ended)
|
||||
} else {
|
||||
q.Ended = ended
|
||||
@@ -589,7 +590,10 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
|
||||
}
|
||||
var rec routerRecord
|
||||
if entry != nil {
|
||||
if err := json.Unmarshal(entry.Value, &rec); err != nil {
|
||||
if err := json.Unmarshal(entry.Value, &rec); err != nil || rec.check() != nil {
|
||||
if err == nil {
|
||||
err = rec.check()
|
||||
}
|
||||
out["state"], out["words"] = AnswerNotProven, "the router's record of it cannot be read: "+err.Error()
|
||||
return out, nil
|
||||
}
|
||||
@@ -613,12 +617,12 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
|
||||
}
|
||||
default:
|
||||
w := *rec.Warrant
|
||||
if w.Outcome != asks.OutcomeChosen {
|
||||
if w.Outcome != occ.OutcomeChosen {
|
||||
out["state"], out["words"] = AnswerEnded, string(w.Outcome)
|
||||
if w.Words != "" {
|
||||
out["words"] = string(w.Outcome) + ": " + w.Words
|
||||
}
|
||||
if w.Outcome == asks.OutcomeExpired {
|
||||
if w.Outcome == occ.OutcomeExpired {
|
||||
out["now"] = "do what you said: " + q.OnExpiry
|
||||
}
|
||||
return out, nil
|
||||
@@ -627,9 +631,9 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
|
||||
out["state"], out["words"] = AnswerNotProven, "the question kept is not one this module asks: "+problem
|
||||
return out, nil
|
||||
}
|
||||
option, err := w.For(Module, q.Ask())
|
||||
option, err := occ.For(w, Module, q.Ask())
|
||||
if err == nil {
|
||||
err = option.Performs(q.Act(option.ID))
|
||||
err = occ.Performs(option, q.Act(option.ID))
|
||||
}
|
||||
if err != nil {
|
||||
say("REFUSED the answer to question %s: %v", q.ID, err)
|
||||
@@ -642,8 +646,9 @@ func ReadAnswer(store QuestionStore, bus AskBus, key string, now time.Time) (map
|
||||
chosen["means"], chosen["recommended"] = o.Means, o.Recommended
|
||||
}
|
||||
}
|
||||
out["state"], out["chosen"], out["by"], out["channel"], out["at"] = AnswerChosen, chosen, w.Says(), w.Channel,
|
||||
w.At.Format(time.RFC3339)
|
||||
at, _ := warrantAt(w) // checked with the record above
|
||||
out["state"], out["chosen"], out["by"], out["channel"], out["at"] = AnswerChosen, chosen, occ.Says(w), w.Channel,
|
||||
at.Format(time.RFC3339)
|
||||
out["note"] = "the operator's word to this session; it authorises no call that needs the operator's approval"
|
||||
}
|
||||
return out, nil
|
||||
@@ -663,3 +668,37 @@ func WaitForAnswer(store QuestionStore, bus AskBus, key string, wait time.Durati
|
||||
time.Sleep(AnswerLooksEvery)
|
||||
}
|
||||
}
|
||||
|
||||
// warrantAt is when a warrant was made; the contract carries it as an RFC 3339 string, and one that does not parse is
|
||||
// refused, as the decode of the router's word was before the contract carried it as a string.
|
||||
func warrantAt(w oc.Warrant) (time.Time, error) {
|
||||
t, err := occ.Time(w.At)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("the warrant's at %q is not a time: %w", w.At, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// askExpires is when an ask ends, read from the contract's RFC 3339 string; one that does not parse is refused.
|
||||
func askExpires(a oc.Ask) (time.Time, error) {
|
||||
t, err := occ.Time(a.Expires)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("the ask's expires %q is not a time: %w", a.Expires, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// check refuses a router record whose warrant or ask carries a time that does not parse.
|
||||
func (r routerRecord) check() error {
|
||||
if r.Warrant != nil {
|
||||
if _, err := warrantAt(*r.Warrant); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r.Ask != nil {
|
||||
if _, err := askExpires(*r.Ask); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
func mergeTonight() QuestionInput {
|
||||
@@ -50,18 +51,18 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
|
||||
if !strings.HasPrefix(key, QuestionPrefix) || !strings.Contains(out["router"].(string), "took the question") {
|
||||
t.Fatalf("answered %v", out)
|
||||
}
|
||||
if len(r.subjects) != 1 || r.subjects[0] != asks.AskSubject(Module) {
|
||||
if len(r.subjects) != 1 || r.subjects[0] != oc.AcceptSubjectAsk(Module) {
|
||||
t.Fatalf("published on %v", r.subjects)
|
||||
}
|
||||
a := r.published[key]
|
||||
if a.Who != asks.Operator || a.OnExpiry != "I leave the pull request open" || !a.Expires.Equal(t0.Add(QuestionDefault)) {
|
||||
if a.Who != occ.Operator || a.OnExpiry != "I leave the pull request open" || a.Expires != occ.FormatTime(t0.Add(QuestionDefault)) {
|
||||
t.Errorf("ask %+v", a)
|
||||
}
|
||||
if len(a.Options) != 2 {
|
||||
t.Fatalf("options %+v", a.Options)
|
||||
}
|
||||
for _, o := range a.Options {
|
||||
if o.Level != asks.Approve || o.Binds == "" {
|
||||
if o.Level != occ.Approve || o.Binds == "" {
|
||||
t.Errorf("option %s is %s binding %q: every option is an approval bound to the question", o.ID, o.Level, o.Binds)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +70,7 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
|
||||
t.Errorf("the recommendation is not on the recommended option alone: %+v", a.Options)
|
||||
}
|
||||
// At a glance (issue 639): the context and the recommendation are the explanation; who asks is the Details.
|
||||
if !strings.HasSuffix(a.Explanation, "\n• Recommended: Merge now, because it is reviewed and the checks are green.") || asks.Glance(a.Explanation) != nil {
|
||||
if !strings.HasSuffix(a.Explanation, "\n• Recommended: Merge now, because it is reviewed and the checks are green.") || occ.Glance(a.Explanation) != nil {
|
||||
t.Errorf("the explanation does not read at a glance:\n%s", a.Explanation)
|
||||
}
|
||||
for _, want := range []string{"the agent session fix-589 on g14", "performs nothing by itself"} {
|
||||
@@ -77,7 +78,7 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
|
||||
t.Errorf("the details do not say %q:\n%s", want, a.Details)
|
||||
}
|
||||
}
|
||||
if err := a.Check(t0); err != nil {
|
||||
if err := occ.Check(a, t0); err != nil {
|
||||
t.Errorf("the ask does not pass the asks contract: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -212,8 +213,8 @@ func (r *questionRefusingRouter) Publish(subject string, body any, id string) er
|
||||
r.id = id
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
HeardQuestionEnd(r.store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused,
|
||||
Words: "its words carry path (a file path), which may not leave the mesh", At: t0})
|
||||
HeardQuestionEnd(r.store, oc.Warrant{Ask: id, Asker: Module, Outcome: occ.OutcomeRefused,
|
||||
Words: "its words carry path (a file path), which may not leave the mesh", At: occ.FormatTime(t0)})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -247,7 +248,7 @@ func TestAnExpiredQuestionSaysWhatTheSessionDoes(t *testing.T) {
|
||||
p, _ := node(t, "g14")
|
||||
store, r := newMemQuestions(), newRouter()
|
||||
key := askMergeTonight(t, p, store, r, t0)
|
||||
r.close(asks.Warrant{Ask: key, Outcome: asks.OutcomeExpired, At: t0.Add(QuestionDefault)})
|
||||
r.close(oc.Warrant{Ask: key, Outcome: occ.OutcomeExpired, At: occ.FormatTime(t0.Add(QuestionDefault))})
|
||||
out, err := ReadAnswer(store, r, key, t0.Add(QuestionDefault+time.Minute))
|
||||
if err != nil || out["state"] != AnswerEnded || out["now"] != "do what you said: I leave the pull request open" {
|
||||
t.Errorf("expired: %v %v", out, err)
|
||||
@@ -276,7 +277,7 @@ func TestTheQuestionToolsAreServed(t *testing.T) {
|
||||
var asked map[string]any
|
||||
_ = json.Unmarshal([]byte(text), &asked)
|
||||
key, _ := asked["key"].(string)
|
||||
if a := r.published[key]; !a.Expires.Equal(t0.Add(30 * time.Minute)) {
|
||||
if a := r.published[key]; a.Expires != occ.FormatTime(t0.Add(30*time.Minute)) {
|
||||
t.Errorf("expires %v", a.Expires)
|
||||
}
|
||||
r.close(r.chose(key, "o1", t0.Add(time.Minute)))
|
||||
@@ -381,10 +382,10 @@ func TestTheEdgesOfAQuestionAreAsked(t *testing.T) {
|
||||
// A warrant naming another asker or another ask, or an option the question did not offer, is no answer to it.
|
||||
func TestAWarrantForAnotherAskOrOptionIsNotProven(t *testing.T) {
|
||||
p, _ := node(t, "g14")
|
||||
for name, change := range map[string]func(*asks.Warrant){
|
||||
"another asker": func(w *asks.Warrant) { w.Asker = "mesh-controller" },
|
||||
"another ask": func(w *asks.Warrant) { w.Ask = "q-0000000000000000" },
|
||||
"an option unoffered": func(w *asks.Warrant) { w.Option, w.Label = "o9", "Delete everything" },
|
||||
for name, change := range map[string]func(*oc.Warrant){
|
||||
"another asker": func(w *oc.Warrant) { w.Asker = "mesh-controller" },
|
||||
"another ask": func(w *oc.Warrant) { w.Ask = "q-0000000000000000" },
|
||||
"an option unoffered": func(w *oc.Warrant) { w.Option, w.Label = "o9", "Delete everything" },
|
||||
} {
|
||||
store, r := newMemQuestions(), newRouter()
|
||||
key := askMergeTonight(t, p, store, r, t0)
|
||||
@@ -474,7 +475,7 @@ func TestASlotIsTakenOnlyAtTheRevisionRead(t *testing.T) {
|
||||
store, r := newMemQuestions(), newRouter()
|
||||
// Two questions that ended: their slots are free to take again.
|
||||
for _, id := range []string{askMergeTonight(t, p, store, r, t0), askMergeTonight(t, p, store, r, t0)} {
|
||||
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeExpired, At: t0.Add(time.Minute)})
|
||||
r.close(oc.Warrant{Ask: id, Outcome: occ.OutcomeExpired, At: occ.FormatTime(t0.Add(time.Minute))})
|
||||
}
|
||||
shanks, _ := node(t, "shanks")
|
||||
other := newRouter()
|
||||
@@ -487,7 +488,7 @@ func TestASlotIsTakenOnlyAtTheRevisionRead(t *testing.T) {
|
||||
From: shanks.Node, At: t0, Expires: t0.Add(time.Hour),
|
||||
Options: []QuestionOption{{ID: "o1", Label: "A", Means: "a", Recommended: true}, {ID: "o2", Label: "B", Means: "b"}}}
|
||||
_ = store.Put(q.ID, q)
|
||||
_ = other.Publish(asks.AskSubject(Module), q.Ask(), q.ID)
|
||||
_ = other.Publish(oc.AcceptSubjectAsk(Module), q.Ask(), q.ID)
|
||||
r.records[q.ID] = other.records[q.ID]
|
||||
_ = store.Put(key, slot{Question: q.ID, At: t0})
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@ module claude-code
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639
|
||||
git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-20261104174344-ae5266e77639
|
||||
git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104174344-ae5266e77639
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-20261104174344-ae5266e77639 h1:19PhtrCOwia711Oqdyafvs5EKTVmmgk5Gdb4leCkq3w=
|
||||
git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-20261104174344-ae5266e77639/go.mod h1:Dj6fbL8Evgy72EWO3Gl3L3Ra8N/nJSzgEVwSGmeRam0=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104174344-ae5266e77639 h1:K/1yIkqAWKGtLEtJ0PibijIdpo5YATL/9IUVXFoEglM=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104174344-ae5266e77639/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
@@ -12,6 +28,8 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"operator-channel"
|
||||
],
|
||||
"invokes": [
|
||||
"seat:mesh-hand-acts.warranted"
|
||||
"seat:mesh-hand-acts.warranted",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"mesh-delivery-indexer.indexer_read_modules",
|
||||
"mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-assigner.assigner_read_placement"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.manifests",
|
||||
"mesh-delivery-assigner.views.placement"
|
||||
],
|
||||
"state": [
|
||||
"servers",
|
||||
@@ -32,13 +41,14 @@
|
||||
"proposals",
|
||||
"questions",
|
||||
"memory",
|
||||
"waits"
|
||||
"waits",
|
||||
"findings"
|
||||
],
|
||||
"emits": [
|
||||
"judged"
|
||||
],
|
||||
"reads": [
|
||||
"claude-licence-manager.bindings",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.manifests"
|
||||
"claude-licence-manager.bindings"
|
||||
],
|
||||
"tools": [
|
||||
"claude_code_status",
|
||||
@@ -89,7 +99,9 @@
|
||||
"claude_code_home_removed",
|
||||
"claude_code_home_restore",
|
||||
"claude_code_judge",
|
||||
"claude_code_judged"
|
||||
"claude_code_judged",
|
||||
"claude_code_first_views",
|
||||
"claude_code_first_view_again"
|
||||
],
|
||||
"data": {
|
||||
"own": [
|
||||
|
||||
@@ -3,7 +3,7 @@ module claude-licence-manager
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -11,8 +13,8 @@ github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module clipmenu
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@ module cups
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
+6
-4
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@ module dbus
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
+6
-4
@@ -1,9 +1,11 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
// Kind is this channel's kind on both benches.
|
||||
@@ -59,19 +59,19 @@ const StandingEvery = 5 * time.Minute
|
||||
|
||||
// Standing is what the desk says of itself: the bench's word, and when a named machine's notifier last lost
|
||||
// the answers its notifications waited for (its bundle started again). The router then says again what is
|
||||
// still open and unanswered (issue 384). It belongs in asks.Standing once the SDK carries it; until then the
|
||||
// still open and unanswered (issue 384). It belongs in channel.Standing once the SDK carries it; until then the
|
||||
// field travels as JSON the router reads under the same name. Zero (as "0001-01-01T00:00:00Z") until then.
|
||||
type Standing struct {
|
||||
asks.Standing
|
||||
channel.Standing
|
||||
AnswersLostAt time.Time `json:"answers-lost-at"`
|
||||
}
|
||||
|
||||
// Shown is one of the router's messages as the desk shows it: the message, and its notification on each
|
||||
// machine.
|
||||
type Shown struct {
|
||||
Message asks.Message `json:"message"`
|
||||
On map[string]int `json:"on"`
|
||||
Sent time.Time `json:"sent"`
|
||||
Message channel.Message `json:"message"`
|
||||
On map[string]int `json:"on"`
|
||||
Sent time.Time `json:"sent"`
|
||||
}
|
||||
|
||||
// Store keeps what is shown, by the router's handle.
|
||||
@@ -146,7 +146,7 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
d.Logf("[desk-channel] work on %s is not this kind's; taken and dropped", subject)
|
||||
return nil
|
||||
}
|
||||
var m asks.Message
|
||||
var m channel.Message
|
||||
if err := json.Unmarshal(body, &m); err != nil || m.Handle == "" {
|
||||
d.Logf("[desk-channel] work on %s could not be read; taken and dropped", subject)
|
||||
return nil
|
||||
@@ -158,8 +158,8 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
// would appear later, to whoever is at the screen then.
|
||||
seeing, why := d.seeing()
|
||||
if len(seeing) == 0 {
|
||||
f := asks.Failed{Handle: m.Handle, Why: "not shown: " + why, At: d.Now()}
|
||||
return d.Publish(asks.IntakeSubject(asks.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10))
|
||||
f := channel.Failed{Handle: m.Handle, Why: "not shown: " + why, At: d.Now()}
|
||||
return d.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10))
|
||||
}
|
||||
machines = seeing
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
// shown (a reminder, an escalation), replace the notification it had on each machine — dunst's replace id —
|
||||
// so a problem is one notification updated in place, never one more beside it. An edit stays quiet where
|
||||
// it was shown; a message said again is shown anew where it was not.
|
||||
edit := parts[4] == asks.Edit
|
||||
edit := parts[4] == channel.Edit
|
||||
var before *Shown
|
||||
switch {
|
||||
case edit:
|
||||
@@ -226,8 +226,8 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
if len(machines) == 0 {
|
||||
why = "no machine to show it on: the setting desktop-machines names none"
|
||||
}
|
||||
f := asks.Failed{Handle: m.Handle, Why: why, At: d.Now()}
|
||||
if err := d.Publish(asks.IntakeSubject(asks.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10)); err != nil {
|
||||
f := channel.Failed{Handle: m.Handle, Why: why, At: d.Now()}
|
||||
if err := d.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -322,7 +322,7 @@ func sameShown(a, b Shown) bool {
|
||||
}
|
||||
|
||||
// show puts one message on one machine's notifier, in place of `replace` when it names one.
|
||||
func (d *Desk) show(machine string, m asks.Message, replace int) (int, error) {
|
||||
func (d *Desk) show(machine string, m channel.Message, replace int) (int, error) {
|
||||
urgency := "normal"
|
||||
switch {
|
||||
case m.Urgent:
|
||||
@@ -376,12 +376,12 @@ func (d *Desk) Chosen(event, machine, ticket string) {
|
||||
}
|
||||
handle, shown := d.find(ticket)
|
||||
if shown == nil {
|
||||
_, _ = d.show(machine, asks.Message{Title: "That answer was not taken",
|
||||
_, _ = d.show(machine, channel.Message{Title: "That answer was not taken",
|
||||
Body: "It belongs to no message shown here any more. Nothing was done.", Silent: true}, 0)
|
||||
return
|
||||
}
|
||||
if _, here := shown.On[machine]; !here {
|
||||
_, _ = d.show(machine, asks.Message{Title: "That answer was not taken",
|
||||
_, _ = d.show(machine, channel.Message{Title: "That answer was not taken",
|
||||
Body: "It was not shown on " + machine + ". Nothing was done.", Silent: true}, 0)
|
||||
return
|
||||
}
|
||||
@@ -395,11 +395,11 @@ func (d *Desk) Chosen(event, machine, ticket string) {
|
||||
}
|
||||
d.clickedOn[id] = machine
|
||||
d.mu.Unlock()
|
||||
c := asks.Choice{ID: id, Ticket: ticket, Handle: handle, At: d.Now(),
|
||||
Sender: asks.Sender{Identity: machine, Display: machine, Authenticated: false}}
|
||||
if err := d.Publish(asks.IntakeSubject(asks.Chosen, Kind), c, id); err != nil {
|
||||
c := channel.Choice{ID: id, Ticket: ticket, Handle: handle, At: d.Now(),
|
||||
Sender: channel.Sender{Identity: machine, Display: machine, Authenticated: false}}
|
||||
if err := d.Publish(channel.IntakeSubject(channel.Chosen, Kind), c, id); err != nil {
|
||||
d.Logf("[desk-channel] a choice could not be said to the router: %v", err)
|
||||
_, _ = d.show(machine, asks.Message{Title: "That answer could not be taken now",
|
||||
_, _ = d.show(machine, channel.Message{Title: "That answer could not be taken now",
|
||||
Body: "The mesh did not take it. Try again in a minute.", Silent: true}, 0)
|
||||
}
|
||||
}
|
||||
@@ -508,7 +508,7 @@ func (d *Desk) seeing() ([]string, string) {
|
||||
// (the review of 2026-10-09). The router shows no link's code while the desk is not ready.
|
||||
func (d *Desk) Standing() Standing {
|
||||
d.init()
|
||||
s := Standing{Standing: asks.Standing{At: d.Now()}, AnswersLostAt: d.answersLostAt()}
|
||||
s := Standing{Standing: channel.Standing{At: d.Now()}, AnswersLostAt: d.answersLostAt()}
|
||||
if _, why := d.seeing(); why != "" {
|
||||
s.Why = why
|
||||
return s
|
||||
@@ -542,7 +542,7 @@ func (d *Desk) SayStanding() {
|
||||
if same {
|
||||
return
|
||||
}
|
||||
if err := d.Publish(asks.IntakeSubject(asks.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
|
||||
if err := d.Publish(channel.IntakeSubject(channel.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
|
||||
d.Logf("[desk-channel] could not say whether it can send: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
type memShown map[string]Shown
|
||||
@@ -62,10 +62,10 @@ func newDesk(t *testing.T, machines ...string) (*Desk, *[]notified, map[string][
|
||||
return d, &shown, said, store
|
||||
}
|
||||
|
||||
func take(t *testing.T, d *Desk, verb string, m asks.Message) {
|
||||
func take(t *testing.T, d *Desk, verb string, m channel.Message) {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(m)
|
||||
if err := d.Take(asks.ChannelSubject(verb, Kind), raw); err != nil {
|
||||
if err := d.Take(channel.ChannelSubject(verb, Kind), raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -74,16 +74,16 @@ func take(t *testing.T, d *Desk, verb string, m asks.Message) {
|
||||
// says a click as a choice the router judges — only on a machine the message was shown on.
|
||||
func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
|
||||
d, shown, said, _ := newDesk(t, "laptop", "away-machine")
|
||||
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "3 failed services", Body: "Needs you: <mend> them.",
|
||||
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "3 failed services", Body: "Needs you: <mend> them.",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
if len(*shown) != 1 || (*shown)[0].machine != "laptop" || (*shown)[0].args["expire_ms"] != 0 ||
|
||||
(*shown)[0].args["body"] != "Needs you: <mend> them." {
|
||||
t.Fatalf("shown %+v", *shown)
|
||||
}
|
||||
d.Chosen("e1", "laptop", "t1")
|
||||
d.Chosen("e1", "laptop", "t1") // a redelivery
|
||||
got := said[asks.IntakeSubject(asks.Chosen, Kind)]
|
||||
var c asks.Choice
|
||||
got := said[channel.IntakeSubject(channel.Chosen, Kind)]
|
||||
var c channel.Choice
|
||||
if len(got) != 1 || json.Unmarshal(got[0], &c) != nil || c.Ticket != "t1" || c.Handle != "m1" ||
|
||||
c.Sender.Identity != "laptop" || c.Sender.Authenticated {
|
||||
t.Fatalf("said %s", got)
|
||||
@@ -91,14 +91,14 @@ func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
|
||||
// Elsewhere it is refused, there, and nothing is said to the router.
|
||||
d.Chosen("e2", "workstation", "t1")
|
||||
d.Chosen("e3", "laptop", "forged")
|
||||
if len(said[asks.IntakeSubject(asks.Chosen, Kind)]) != 1 {
|
||||
if len(said[channel.IntakeSubject(channel.Chosen, Kind)]) != 1 {
|
||||
t.Error("a click not shown there was said")
|
||||
}
|
||||
if last := (*shown)[len(*shown)-1]; last.machine != "laptop" || last.args["summary"] != "That answer was not taken" {
|
||||
t.Errorf("the refusal was not shown where it was chosen: %+v", last)
|
||||
}
|
||||
// The router's reply to the click is shown on the machine it was made on.
|
||||
take(t, d, asks.Send, asks.Message{Handle: "r1", Title: "Details: 3 failed services", Reply: c.ID})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "r1", Title: "Details: 3 failed services", Reply: c.ID})
|
||||
if last := (*shown)[len(*shown)-1]; last.machine != "laptop" || last.args["summary"] != "Details: 3 failed services" {
|
||||
t.Errorf("%+v", last)
|
||||
}
|
||||
@@ -107,8 +107,8 @@ func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
|
||||
// The note follows the body after a blank line: the desk has no smaller text, and leaves nothing out (issue 639).
|
||||
func TestTheNoteFollowsTheBody(t *testing.T) {
|
||||
d, shown, _, _ := newDesk(t, "laptop")
|
||||
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "q", Body: "The summary.", Note: "Asked by mesh-knowledge at 16:05.",
|
||||
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "q", Body: "The summary.", Note: "Asked by mesh-knowledge at 16:05.",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
if last := (*shown)[len(*shown)-1]; last.args["body"] != "The summary.\n\nAsked by mesh-knowledge at 16:05." {
|
||||
t.Errorf("the body: %q", last.args["body"])
|
||||
}
|
||||
@@ -116,8 +116,8 @@ func TestTheNoteFollowsTheBody(t *testing.T) {
|
||||
|
||||
func TestAnEditReplacesTheNotification(t *testing.T) {
|
||||
d, shown, _, _ := newDesk(t, "laptop")
|
||||
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "q", Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, asks.Edit, asks.Message{Handle: "m1", Title: "Silence for a week: chosen"})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "q", Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, channel.Edit, channel.Message{Handle: "m1", Title: "Silence for a week: chosen"})
|
||||
if last := (*shown)[len(*shown)-1]; last.args["replace_id"] != 1 || last.args["actions"] != nil {
|
||||
t.Errorf("the edit: %+v", last.args)
|
||||
}
|
||||
@@ -134,17 +134,17 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
|
||||
d, shown, said, store := newDesk(t, "laptop")
|
||||
d.Paused("laptop", false)
|
||||
d.SayStanding()
|
||||
take(t, d, asks.Show, asks.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
take(t, d, asks.Show, asks.Message{Handle: "silenced", Title: "Agent-home shrank",
|
||||
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t-silenced"}}})
|
||||
take(t, d, asks.Edit, asks.Message{Handle: "silenced", Title: "Silenced until Saturday: Agent-home shrank", Silent: true})
|
||||
take(t, d, asks.Show, asks.Message{Handle: "resolved", Title: "Questions for you not delivered",
|
||||
Buttons: []asks.Button{{Label: "Details", Ticket: "t-resolved"}}})
|
||||
take(t, d, asks.Edit, asks.Message{Handle: "resolved", Title: "Resolved: questions for you are delivered", Silent: true})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "silenced", Title: "Agent-home shrank",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-silenced"}}})
|
||||
take(t, d, channel.Edit, channel.Message{Handle: "silenced", Title: "Silenced until Saturday: Agent-home shrank", Silent: true})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "resolved", Title: "Questions for you not delivered",
|
||||
Buttons: []channel.Button{{Label: "Details", Ticket: "t-resolved"}}})
|
||||
take(t, d, channel.Edit, channel.Message{Handle: "resolved", Title: "Resolved: questions for you are delivered", Silent: true})
|
||||
openID := store["open"].On["laptop"]
|
||||
before := len(*shown)
|
||||
standings := len(said[asks.IntakeSubject(asks.StandingWhat, Kind)])
|
||||
standings := len(said[channel.IntakeSubject(channel.StandingWhat, Kind)])
|
||||
later := d.Now().Add(time.Hour)
|
||||
d.Now = func() time.Time { return later }
|
||||
d.Paused("laptop", false)
|
||||
@@ -157,25 +157,25 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
|
||||
if store["open"].On["laptop"] != openID {
|
||||
t.Fatalf("the open message's notification was forgotten: %+v", store["open"].On)
|
||||
}
|
||||
got := said[asks.IntakeSubject(asks.StandingWhat, Kind)]
|
||||
got := said[channel.IntakeSubject(channel.StandingWhat, Kind)]
|
||||
var s Standing
|
||||
if len(got) != standings+1 || json.Unmarshal(got[len(got)-1], &s) != nil || !s.Ready || !s.AnswersLostAt.Equal(later) {
|
||||
t.Fatalf("the standing after the start: %d said, last %s", len(got)-standings, got[len(got)-1])
|
||||
}
|
||||
// The router says the open one again under its handle: in place of its notification, with its answers.
|
||||
take(t, d, asks.Show, asks.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
last := (*shown)[len(*shown)-1]
|
||||
if len(*shown) != before+1 || last.args["replace_id"] != openID || last.args["actions"] == nil {
|
||||
t.Fatalf("the open message said again: %+v", last.args)
|
||||
}
|
||||
d.Chosen("e2", "laptop", "t-open")
|
||||
if len(said[asks.IntakeSubject(asks.Chosen, Kind)]) != 1 {
|
||||
if len(said[channel.IntakeSubject(channel.Chosen, Kind)]) != 1 {
|
||||
t.Error("a click on the message said again was not said to the router")
|
||||
}
|
||||
// The same start event again (a redelivery) does nothing more.
|
||||
standings = len(said[asks.IntakeSubject(asks.StandingWhat, Kind)])
|
||||
if n := d.Started("e-start", "laptop"); n != 0 || len(said[asks.IntakeSubject(asks.StandingWhat, Kind)]) != standings {
|
||||
standings = len(said[channel.IntakeSubject(channel.StandingWhat, Kind)])
|
||||
if n := d.Started("e-start", "laptop"); n != 0 || len(said[channel.IntakeSubject(channel.StandingWhat, Kind)]) != standings {
|
||||
t.Errorf("a redelivered start was acted on again: %d", n)
|
||||
}
|
||||
// A desk none of whose notifiers started again says a zero time, which the router reads as none.
|
||||
@@ -187,8 +187,8 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
|
||||
|
||||
func TestNoMachineThatShowsItIsAFailureSaidToTheRouter(t *testing.T) {
|
||||
d, _, said, _ := newDesk(t, "away-machine")
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "x"})
|
||||
if f := said[asks.IntakeSubject(asks.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "no session answers") ||
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "x"})
|
||||
if f := said[channel.IntakeSubject(channel.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "no session answers") ||
|
||||
strings.Contains(string(f[0]), `"permanent":true`) {
|
||||
t.Errorf("%s", f)
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func TestItPromisesNoVerifiedSender(t *testing.T) {
|
||||
func TestASecretMessageIsShownAndNotKept(t *testing.T) {
|
||||
d, shown, _, store := newDesk(t, "laptop")
|
||||
d.Paused("laptop", false)
|
||||
take(t, d, asks.Send, asks.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true})
|
||||
if len(*shown) != 1 {
|
||||
t.Fatal("not shown")
|
||||
}
|
||||
@@ -252,8 +252,8 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
|
||||
if s := d.Standing(); !s.Ready {
|
||||
t.Fatalf("not ready with laptop heard unpaused: %+v", s)
|
||||
}
|
||||
code := asks.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}
|
||||
take(t, d, asks.Send, code)
|
||||
code := channel.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}
|
||||
take(t, d, channel.Send, code)
|
||||
if len(*shown) != 1 || (*shown)[0].machine != "laptop" || (*shown)[0].args["secret"] != true {
|
||||
t.Fatalf("the code was shown as %+v", *shown)
|
||||
}
|
||||
@@ -262,11 +262,11 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
|
||||
}
|
||||
// Paused: not shown anywhere, said as failed, and the desk is not ready.
|
||||
d.Paused("laptop", true)
|
||||
take(t, d, asks.Send, code)
|
||||
take(t, d, channel.Send, code)
|
||||
if len(*shown) != 1 {
|
||||
t.Fatalf("the code was shown on a paused notifier: %+v", *shown)
|
||||
}
|
||||
if f := said[asks.IntakeSubject(asks.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "paused") {
|
||||
if f := said[channel.IntakeSubject(channel.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "paused") {
|
||||
t.Errorf("not said as failed: %s", f)
|
||||
}
|
||||
if s := d.Standing(); s.Ready || !strings.Contains(s.Why, "paused") {
|
||||
@@ -284,7 +284,7 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
|
||||
t.Error("a machine not named made the desk ready")
|
||||
}
|
||||
// A message that is not secret still goes to every named machine, paused or not.
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m9", Title: "x"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m9", Title: "x"})
|
||||
if len(*shown) != 3 {
|
||||
t.Errorf("an ordinary message was held back: %+v", *shown)
|
||||
}
|
||||
@@ -330,8 +330,8 @@ func TestItRunsAsItsOwnAccount(t *testing.T) {
|
||||
// was not shown; a message under a new handle replaces nothing.
|
||||
func TestAMessageSaidAgainUnderItsHandleReplacesItsNotification(t *testing.T) {
|
||||
d, shown, _, store := newDesk(t, "laptop", "desktop")
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m2", Title: "another problem"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m2", Title: "another problem"})
|
||||
if len(*shown) != 4 {
|
||||
t.Fatalf("shown %d", len(*shown))
|
||||
}
|
||||
@@ -343,8 +343,8 @@ func TestAMessageSaidAgainUnderItsHandleReplacesItsNotification(t *testing.T) {
|
||||
first := store["m1"].On
|
||||
// The desk now names a third machine as well.
|
||||
d.Machines = func() []string { return []string{"laptop", "desktop", "third"} }
|
||||
take(t, d, asks.Show, asks.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks", Urgent: true,
|
||||
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks", Urgent: true,
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
again := (*shown)[4:]
|
||||
if len(again) != 3 {
|
||||
t.Fatalf("said again on %d machines", len(again))
|
||||
@@ -397,18 +397,18 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
|
||||
d, shown, _, store := newDesk(t, "laptop")
|
||||
down := true
|
||||
d.Store = downStore{store, &down}
|
||||
raised, _ := json.Marshal(asks.Message{Handle: "m9", Title: "A new build cannot be delivered", Urgent: true})
|
||||
if err := d.Take(asks.ChannelSubject(asks.Send, Kind), raised); err != nil {
|
||||
raised, _ := json.Marshal(channel.Message{Handle: "m9", Title: "A new build cannot be delivered", Urgent: true})
|
||||
if err := d.Take(channel.ChannelSubject(channel.Send, Kind), raised); err != nil {
|
||||
t.Fatalf("the raise was not taken: %v", err)
|
||||
}
|
||||
cleared, _ := json.Marshal(asks.Message{Handle: "m9", Title: "Cleared: a new build can be delivered"})
|
||||
if err := d.Take(asks.ChannelSubject(asks.Edit, Kind), cleared); err != nil {
|
||||
cleared, _ := json.Marshal(channel.Message{Handle: "m9", Title: "Cleared: a new build can be delivered"})
|
||||
if err := d.Take(channel.ChannelSubject(channel.Edit, Kind), cleared); err != nil {
|
||||
t.Fatalf("the clearing was not taken: %v", err)
|
||||
}
|
||||
d.KeepUnkept() // the minute's standing, store still down
|
||||
down = !back
|
||||
d.KeepUnkept()
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m10", Title: "another"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m10", Title: "another"})
|
||||
if len(*shown) != 3 || (*shown)[1].args["replace_id"] != 1 || (*shown)[1].args["summary"] != "Cleared: a new build can be delivered" {
|
||||
t.Fatalf("the clearing did not replace the urgent notification: %+v", *shown)
|
||||
}
|
||||
@@ -423,7 +423,7 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
|
||||
}
|
||||
|
||||
d, shown, said, store := newDesk(t, "laptop", "desktop")
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
first := store["m1"].On
|
||||
notify := d.Notify
|
||||
d.Notify = func(machine string, args map[string]any) (json.RawMessage, error) {
|
||||
@@ -432,13 +432,13 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
|
||||
}
|
||||
return notify(machine, args)
|
||||
}
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks"})
|
||||
if got := store["m1"].On["desktop"]; got != first["desktop"] {
|
||||
t.Fatalf("the desktop's notification %d was forgotten: %v", first["desktop"], store["m1"].On)
|
||||
}
|
||||
d.Notify = func(string, map[string]any) (json.RawMessage, error) { return nil, errors.New("away") }
|
||||
take(t, d, asks.Send, asks.Message{Handle: "m1", Title: "Now urgent: 2 failed services on shanks"})
|
||||
if len(said[asks.IntakeSubject(asks.FailedWhat, Kind)]) != 1 {
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "Now urgent: 2 failed services on shanks"})
|
||||
if len(said[channel.IntakeSubject(channel.FailedWhat, Kind)]) != 1 {
|
||||
t.Fatalf("shown nowhere, and not said as failed")
|
||||
}
|
||||
_ = shown
|
||||
|
||||
@@ -2,10 +2,10 @@ module deskchannel
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -3,20 +3,21 @@ module diskload
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.2 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
)
|
||||
|
||||
+12
-10
@@ -1,15 +1,17 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
@@ -23,5 +25,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
|
||||
@@ -59,7 +59,7 @@ func tagsAnswer(v *holder.Verbs, a holder.VersionsAnswer) reg.TagsAnswer {
|
||||
return out
|
||||
}
|
||||
|
||||
// eventBody is an event's body as the seat's contract defines it: an Outcome for published, refused and removed, a
|
||||
// eventBody is an event's body as the seat's contract defines it: an Outcome for published, refused, removed and kept, a
|
||||
// Hold for held and released.
|
||||
func eventBody(event string, e holder.Event) any {
|
||||
switch event {
|
||||
@@ -67,5 +67,5 @@ func eventBody(event string, e holder.Event) any {
|
||||
return reg.Hold{Name: e.Name, Digest: e.Digest, Holder: e.Holder, By: e.By, Reason: e.Reason, At: time.Now().UTC().Format(time.RFC3339)}
|
||||
}
|
||||
return reg.Outcome{Name: e.Name, Version: e.Version, Kind: e.Kind, Tag: e.Tag, Digest: e.Digest, Fingerprint: e.Fingerprint,
|
||||
Commit: e.Commit, Holder: e.Holder, Transfer: e.Transfer, Reference: e.Reference, Reason: e.Reason}
|
||||
Commit: e.Commit, Holder: e.Holder, Transfer: e.Transfer, Reference: e.Reference, Reason: e.Reason, Tags: e.Tags}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestTheVerbsSpeakTheSeatsContract(t *testing.T) {
|
||||
if seen != 14 {
|
||||
t.Errorf("%d seat verbs served, want 14 (8 on the image seat, 6 on the artifact seat)", seen)
|
||||
}
|
||||
for event, want := range map[string]any{holder.EventPublished: ®.Outcome{}, holder.EventHeld: ®.Hold{}, holder.EventReleased: ®.Hold{}} {
|
||||
for event, want := range map[string]any{holder.EventPublished: ®.Outcome{}, holder.EventKept: ®.KeptEvent{}, holder.EventHeld: ®.Hold{}, holder.EventReleased: ®.Hold{}} {
|
||||
b, _ := json.Marshal(eventBody(event, holder.Event{Name: "m/a", Kind: holder.KindArchive, Digest: "sha256:aa", Holder: holder.Self, By: "x.artifact-store-tools"}))
|
||||
dec := json.NewDecoder(bytes.NewReader(b))
|
||||
dec.DisallowUnknownFields()
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/parts"
|
||||
|
||||
@@ -80,27 +81,6 @@ func finding(kind, subject, words string, raised bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// closeBootstrap asks the program to discard the bootstrap's one-off credential once a publish is answered (design 51
|
||||
// *On a fresh mesh*): the window bootstrap-push closed in the state windows, which the program follows and then removes
|
||||
// the file. The tools bundle runs in the tool runner's account, which cannot remove a file in distribution's state
|
||||
// directory. Once per process; the window's record says it to the next.
|
||||
var closeBootstrap func()
|
||||
|
||||
func bootstrapCloser(windows kept[holder.Window]) func() {
|
||||
return sync.OnceFunc(func() {
|
||||
cur, _, err := windows.Get(holder.BootstrapWindow)
|
||||
if err == nil && !cur.Closed.IsZero() {
|
||||
return
|
||||
}
|
||||
w := holder.Window{Name: holder.BootstrapWindow, Closed: time.Now().UTC()}
|
||||
if err := windows.Put(w); err != nil {
|
||||
logf("the bootstrap's one-off credential could not be asked away: %v", err)
|
||||
return
|
||||
}
|
||||
logf("the bootstrap's one-off credential is asked away: the holders serve publish")
|
||||
})
|
||||
}
|
||||
|
||||
// learned are the proved callers whose build this process recorded as publishing through the seats.
|
||||
var learned sync.Map
|
||||
|
||||
@@ -185,6 +165,12 @@ func learnInto(state casState, module, fp string, attempts int) (bool, error) {
|
||||
return false, fmt.Errorf("%s was written by another writer first %d times: %w", holder.PublishingKey, attempts, stdio.ErrChanged)
|
||||
}
|
||||
|
||||
// recordAct records a hand act through the seat mesh-hand-acts alone.
|
||||
func recordAct(q handacts.Request) error {
|
||||
_, err := handacts.Recorder{Ask: stdio.Ask}.Record(q)
|
||||
return err
|
||||
}
|
||||
|
||||
// mirrors is the state mirrors: an ask is created once; one already kept is the same copy, taken on already.
|
||||
type mirrors struct{}
|
||||
|
||||
@@ -202,6 +188,28 @@ func (mirrors) Claim(key string, r holder.MirrorRequest, revision uint64) (bool,
|
||||
return err == nil, err
|
||||
}
|
||||
func (mirrors) Delete(key string) error { return stdio.State(holder.MirrorsState).Delete(key) }
|
||||
func (mirrors) List() (map[string]holder.MirrorRequest, error) {
|
||||
keys, err := stdio.State(holder.MirrorsState).Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]holder.MirrorRequest{}
|
||||
for _, k := range keys {
|
||||
e, err := stdio.State(holder.MirrorsState).Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r holder.MirrorRequest
|
||||
if e != nil && json.Unmarshal(e.Value, &r) == nil {
|
||||
out[k] = r
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (mirrors) Put(key string, r holder.MirrorRequest) error {
|
||||
_, err := stdio.State(holder.MirrorsState).Put(key, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// followMirrors runs every copy `mirror` took on, on the state's watch: the asks kept when the bundle starts (a copy a
|
||||
// restart cut) and each new one, one at a time. Each is claimed first (Verbs.Take), so a second follower copies none
|
||||
@@ -266,11 +274,7 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = v.DropIdle()
|
||||
a, err := v.Publish(seat, stdio.CallerOf(args), publishOf(in))
|
||||
if err == nil && closeBootstrap != nil {
|
||||
closeBootstrap()
|
||||
}
|
||||
if err == nil {
|
||||
learnPublishing(windows, stdio.CallerOf(args))
|
||||
}
|
||||
@@ -318,10 +322,11 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
|
||||
return nil, err
|
||||
}
|
||||
r := holder.RemoveRequest{Name: in.Name, Version: in.Version, Digest: in.Digest, Reason: in.Reason}
|
||||
if err := v.Remove(seat, stdio.CallerOf(args), r); err != nil {
|
||||
got, err := v.Remove(seat, stdio.CallerOf(args), r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reg.RemoveAnswer{Removed: true, Artifact: ®.Artifact{Name: in.Name, Version: in.Version, Tag: in.Tag, Digest: in.Digest}}, nil
|
||||
return reg.RemoveAnswer{Removed: got.Removed, Kept: got.Kept, Reason: got.Reason, Tags: got.Tags, Artifact: ®.Artifact{Name: in.Name, Version: in.Version, Tag: in.Tag, Digest: in.Digest}}, nil
|
||||
}},
|
||||
stdio.Tool{Name: seat + "." + holder.VerbHold, Description: "Keep one untagged bundle, archive or copied base from retention (the artifact sweep alone); says held.",
|
||||
Input: obj(map[string]any{"name": s("name"), "digest": s("sha256:…"), "reason": s("which of the sweep's rules keeps it")}, "name", "digest", "reason"),
|
||||
@@ -412,16 +417,42 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
|
||||
}
|
||||
idle, _ := v.DropIdle()
|
||||
if id := str(args, "drop"); id != "" {
|
||||
if str(args, "why") == "" {
|
||||
return nil, errors.New("a drop is a hand act: say why")
|
||||
t, err := v.DropByHand(id, stdio.CallerOf(args), str(args, "why"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err := v.Drop(id)
|
||||
logf("transfer %s dropped by hand by %s: %s", id, stdio.CallerOf(args), str(args, "why"))
|
||||
return map[string]any{"dropped": t.ID}, err
|
||||
return map[string]any{"dropped": t.ID}, nil
|
||||
}
|
||||
all, err := v.Transfers.List()
|
||||
used, _ := v.Objects.Used()
|
||||
return map[string]any{"transfers": all, "used": used, "cap": holder.IncomingCap, "dropped_idle": idle}, err
|
||||
return map[string]any{"transfers": all, "used": used, "cap": v.CapNow(), "dropped_idle": idle}, err
|
||||
}},
|
||||
stdio.Tool{Name: "registry_mirrors", Description: "The copies of outside images mirror took on and has not ended (the state mirrors): each by its key, its source, digest, who asked and the follower that claimed it (none: it waits). With again, asks one again by hand, its claim let go so a follower takes it on; with drop, drops one by hand, saying refused on the image seat's events so its waiter ends (each a hand act, why required).",
|
||||
Input: obj(map[string]any{"again": s("a kept copy's key to ask again"), "drop": s("a kept copy's key to drop"), "why": s("why (a hand act)")}),
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
by, why := stdio.CallerOf(args), str(args, "why")
|
||||
switch {
|
||||
case str(args, "again") != "":
|
||||
if why == "" {
|
||||
return nil, errors.New("asking a copy again is a hand act: say why")
|
||||
}
|
||||
r, err := v.MirrorAgain(str(args, "again"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logf("the copy %s asked again by hand by %s: %s", str(args, "again"), by, why)
|
||||
return map[string]any{"asked_again": str(args, "again"), "copy": r}, nil
|
||||
case str(args, "drop") != "":
|
||||
r, err := v.MirrorDrop(str(args, "drop"), by, why)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logf("the copy %s dropped by hand by %s: %s", str(args, "drop"), by, why)
|
||||
return map[string]any{"dropped": str(args, "drop"), "copy": r}, nil
|
||||
}
|
||||
all, err := v.MirrorsNow()
|
||||
return map[string]any{"mirrors": all}, err
|
||||
}},
|
||||
stdio.Tool{Name: "registry_holds", Description: "What is held, since when and for whom; with release, lets one go through the seat's release as the operator's session (a hand act).",
|
||||
Input: obj(map[string]any{"seat": s("the seat"), "release": s("the name to release"), "digest": s("its digest"), "why": s("why")}),
|
||||
@@ -465,12 +496,7 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
b, _, err := windows.Get(holder.BootstrapWindow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The program removes the file when this window closes and writes it closed again with its removal's time.
|
||||
return map[string]any{"window": cur, "bootstrap_credential_discarded": !b.Closed.IsZero(), "bootstrap": b}, nil
|
||||
return map[string]any{"window": cur}, nil
|
||||
}})
|
||||
}
|
||||
return out
|
||||
@@ -480,11 +506,17 @@ func main() {
|
||||
base, _ := url.Parse("http://127.0.0.1:5001")
|
||||
store := &holder.OCI{Base: base, Upstream: &holder.Upstream{}}
|
||||
h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: delivery.BuilderVersions{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding,
|
||||
Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState)}
|
||||
Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Record: recordAct, Idle: parts.BusArm(holder.IdleState)}
|
||||
v := &holder.Verbs{Holder: h, Catalog: store, Holds: kept[holder.Hold]{state: "holds", key: holder.Hold.Key}, Mirrors: mirrors{}}
|
||||
go followMirrors(v)
|
||||
go parts.FollowIdle(holder.IdleState, parts.IDs(h.Transfers), h.Expired, func(id string, dropped bool, err error) {
|
||||
if err != nil {
|
||||
logf("%v", err)
|
||||
return
|
||||
}
|
||||
logf("transfer %s had no part for %s: dropped with its parts", id, holder.TransferIdle)
|
||||
})
|
||||
windows := kept[holder.Window]{state: "windows", key: func(w holder.Window) string { return w.Name }}
|
||||
closeBootstrap = bootstrapCloser(windows)
|
||||
if err := stdio.Serve("", tools(v, windows, holder.ConfiguredCallers)); err != nil {
|
||||
logf("%v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Command distribution is distribution's program (novox/hq design 51): it serves the artifact store's port in front of
|
||||
// the registry container, which listens on its node's loopback only. Reads (GET, HEAD) go through to every node's
|
||||
// container runtime; a write is refused but while the direct-write window is open, never over a tag the seat holds
|
||||
// published, or with the bootstrap's one-off credential (holder.Front). The windows are distribution's state windows:
|
||||
// published (holder.Front). The windows are distribution's state windows:
|
||||
// the program moves them (windows.go) and follows the direct-write window's record on each change; it holds no timer.
|
||||
package main
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -58,7 +57,6 @@ func run(ctx context.Context, getenv func(string) string) error {
|
||||
windows := process.OptionalBucket(ctx, js, "distribution_windows", "the direct-write window is read as open until the program starts again", logger.Printf)
|
||||
if windows != nil {
|
||||
go follow(ctx, windows, &open, logger)
|
||||
go followBootstrap(ctx, windows, logger)
|
||||
calls, err := process.Connect(cred)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -72,13 +70,6 @@ func run(ctx context.Context, getenv func(string) string) error {
|
||||
Published: func(name, tag string) (bool, error) {
|
||||
d, err := oci.Digest(name, tag)
|
||||
return d != "", err
|
||||
},
|
||||
Bootstrap: func() (string, bool) {
|
||||
b, err := os.ReadFile(holder.BootstrapFile)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(string(b)), true
|
||||
}}
|
||||
srv := &http.Server{Addr: or(getenv("MESH_REGISTRY_LISTEN"), ":5000"), Handler: front, ReadHeaderTimeout: 30 * time.Second}
|
||||
errs := make(chan error, 1)
|
||||
@@ -119,42 +110,6 @@ func follow(ctx context.Context, kv jetstream.KeyValue, open *atomic.Bool, logge
|
||||
}
|
||||
}
|
||||
|
||||
// followBootstrap removes the bootstrap's one-off credential (holder.BootstrapFile) when the tools bundle closes the
|
||||
// window bootstrap-push at its first answered publish (design 51 *On a fresh mesh*): the program runs as distribution's
|
||||
// account, which owns the file. A file already gone is no fault; one that cannot be removed is said each time the
|
||||
// window is written.
|
||||
func followBootstrap(ctx context.Context, kv jetstream.KeyValue, logger *log.Logger) {
|
||||
w, err := kv.Watch(ctx, holder.BootstrapWindow)
|
||||
if err != nil {
|
||||
logger.Printf("the bootstrap's credential window cannot be watched (%v): its file stays until the program starts again", err)
|
||||
return
|
||||
}
|
||||
for e := range w.Updates() {
|
||||
if e == nil || e.Operation() != jetstream.KeyValuePut {
|
||||
continue
|
||||
}
|
||||
var win holder.Window
|
||||
if json.Unmarshal(e.Value(), &win) != nil || win.Closed.IsZero() {
|
||||
continue
|
||||
}
|
||||
if why := removeBootstrap(holder.BootstrapFile); why != "" {
|
||||
logger.Printf("%s", why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeBootstrap removes the credential's file; it answers what went wrong, or "" when the file is gone.
|
||||
func removeBootstrap(path string) string {
|
||||
switch err := os.Remove(path); {
|
||||
case err == nil:
|
||||
return "the bootstrap's one-off credential is discarded: the holders serve publish"
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return ""
|
||||
default:
|
||||
return fmt.Sprintf("the bootstrap's one-off credential could not be discarded, so it is still accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func or(v, d string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return d
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The program, which runs as distribution's account, removes the bootstrap's one-off credential when its window
|
||||
// closes (design 51 *On a fresh mesh*; a3-publish-contract): removed once, a file already gone is no fault, and one
|
||||
// that cannot be removed is said, since the front still accepts it.
|
||||
func TestTheProgramDiscardsTheBootstrapCredential(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "bootstrap-push")
|
||||
if err := os.WriteFile(file, []byte("token\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if why := removeBootstrap(file); !strings.Contains(why, "discarded") {
|
||||
t.Fatalf("first removal = %q, want it said discarded", why)
|
||||
}
|
||||
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
t.Fatalf("the file is still there: %v", err)
|
||||
}
|
||||
if why := removeBootstrap(file); why != "" {
|
||||
t.Fatalf("a file already gone = %q, want nothing said", why)
|
||||
}
|
||||
stuck := filepath.Join(dir, "stuck")
|
||||
if err := os.MkdirAll(filepath.Join(stuck, "child"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if why := removeBootstrap(stuck); !strings.Contains(why, "still accepted") {
|
||||
t.Fatalf("an unremovable file = %q, want it said still accepted", why)
|
||||
}
|
||||
}
|
||||
@@ -91,9 +91,12 @@ func (d busDelivery) Reads(node string) (bool, []string, error) {
|
||||
raw, err := d.c.CallOn(node, readsAsk, map[string]any{})
|
||||
if err != nil {
|
||||
if notServed(err) {
|
||||
return false, nil, nil // a node-engine not serving manifest-reads states nothing (design 51)
|
||||
// A node-engine not serving manifest-reads states nothing (design 51); what the call answered (an away node,
|
||||
// an older node-engine, a timeout) goes with it, so the window's tool says why the node holds it.
|
||||
return false, nil, err
|
||||
}
|
||||
return false, nil, err
|
||||
// Any other failure is a read that did not happen: served true with its error, so the wake judges nothing.
|
||||
return true, nil, err
|
||||
}
|
||||
var a struct {
|
||||
Reads []string `json:"manifest-reads"`
|
||||
|
||||
@@ -3,21 +3,22 @@ module distribution
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.2 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
)
|
||||
|
||||
+14
-10
@@ -1,15 +1,19 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639 h1:5HeZzk6tKzcSwVivD3IcIvQ/2WIvysZvNvNKyq/oIBA=
|
||||
git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
|
||||
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I=
|
||||
github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
@@ -23,5 +27,5 @@ golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZ
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
|
||||
@@ -3,6 +3,7 @@ package holder
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -44,7 +45,8 @@ type NodeHave struct {
|
||||
|
||||
// Delivery is what the windows' driver asks of the delivery parts and the node-engine seat, through their tools and
|
||||
// verbs on the bus (ADR 0596): every node's haves, away ones included, and one node-engine's stated reader
|
||||
// capabilities. Reads answers served false when the node-engine does not serve `manifest-reads` (it states nothing).
|
||||
// capabilities. Reads answers served false when the node-engine does not serve `manifest-reads` (it states nothing),
|
||||
// with the call's error, if any, saying why (an away node, an older node-engine, a timeout).
|
||||
type Delivery interface {
|
||||
Haves() ([]NodeHave, error)
|
||||
Reads(node string) (served bool, stated []string, err error)
|
||||
@@ -118,6 +120,9 @@ func (d *Driver) Read() ([]NodeRead, error) {
|
||||
return nil, fmt.Errorf("%s's manifest-reads cannot be read, so no window moves: %w", node, err)
|
||||
}
|
||||
n.Serves, n.Stated = served, stated
|
||||
if !served && err != nil {
|
||||
n.NotServed = err.Error()
|
||||
}
|
||||
out = append(out, *n)
|
||||
}
|
||||
return out, nil
|
||||
@@ -131,13 +136,20 @@ func (d *Driver) Wake(why string) ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
now := d.now()
|
||||
named := make([]string, 0, len(nodes))
|
||||
byNode := map[string]NodeRead{}
|
||||
for _, n := range nodes {
|
||||
named = append(named, n.Node)
|
||||
byNode[n.Node] = n
|
||||
}
|
||||
var said, errs []string
|
||||
for _, c := range []struct {
|
||||
name, event, seat string
|
||||
holding func([]NodeRead, []DroppedNode) []string
|
||||
why func(NodeRead) string
|
||||
}{
|
||||
{OldNameWindow, OldNameClosedEvent, OldSeat, HoldingOldName},
|
||||
{DirectWritesWindow, DirectWritesClosedEvent, "", HoldingDirectWrites},
|
||||
{OldNameWindow, OldNameClosedEvent, OldSeat, HoldingOldName, WhyOldName},
|
||||
{DirectWritesWindow, DirectWritesClosedEvent, "", HoldingDirectWrites, WhyDirectWrites},
|
||||
} {
|
||||
cur, err := d.State.Window(c.name)
|
||||
if err != nil {
|
||||
@@ -145,9 +157,16 @@ func (d *Driver) Wake(why string) ([]string, error) {
|
||||
continue
|
||||
}
|
||||
if cur.Name == "" {
|
||||
cur = Window{Name: c.name, Open: true}
|
||||
cur = Window{Name: c.name}
|
||||
cur.Open = true
|
||||
}
|
||||
next, closed, overdue := cur.Step(named, c.holding(nodes, cur.Dropped), now)
|
||||
if next.Open && len(named) > 0 {
|
||||
next.Why = map[string]string{}
|
||||
for _, n := range next.Holding {
|
||||
next.Why[n] = c.why(byNode[n])
|
||||
}
|
||||
}
|
||||
next, closed, overdue := cur.Step(c.holding(nodes, cur.Dropped), now)
|
||||
if !windowsEqual(cur, next) {
|
||||
if err := d.State.PutWindow(next); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("the %s window is not kept: %v", c.name, err))
|
||||
@@ -172,6 +191,8 @@ func (d *Driver) Wake(why string) ([]string, error) {
|
||||
}
|
||||
}
|
||||
said = append(said, fmt.Sprintf("the %s window is closed (%s); it was held last by %v", c.name, why, closed.LastHolding))
|
||||
} else if len(named) == 0 {
|
||||
said = append(said, fmt.Sprintf("the %s window is not judged (%s): the read named no node", c.name, why))
|
||||
} else if next.Open {
|
||||
said = append(said, fmt.Sprintf("the %s window stays open (%s): held by %v", c.name, why, next.Holding))
|
||||
}
|
||||
@@ -191,5 +212,5 @@ func (d *Driver) now() time.Time {
|
||||
|
||||
func windowsEqual(a, b Window) bool {
|
||||
return a.Open == b.Open && slices.Equal(a.Holding, b.Holding) && a.Closed.Equal(b.Closed) &&
|
||||
len(a.Since) == len(b.Since) && len(a.Dropped) == len(b.Dropped)
|
||||
len(a.Since) == len(b.Since) && len(a.Dropped) == len(b.Dropped) && maps.Equal(a.Why, b.Why)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package holder
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -15,7 +14,8 @@ import (
|
||||
// protocol's GET and HEAD) stay open to every node's container runtime; a write reaches the store only from
|
||||
// distribution's own process, which writes to the loopback directly and never through the front. While the direct-write
|
||||
// window is open, a build module's old build still pushes through the front as today, but never to a tag the seat holds
|
||||
// published.
|
||||
// published. No credential opens it: on a fresh mesh the bootstrap pushes the holders' own images to the store on its
|
||||
// loopback, never through the front (design 51 *On a fresh mesh*).
|
||||
type Front struct {
|
||||
// Store is the registry container's loopback address.
|
||||
Store *url.URL
|
||||
@@ -23,9 +23,6 @@ type Front struct {
|
||||
Window *atomic.Bool
|
||||
// Published says whether name holds a tag published through the seat.
|
||||
Published func(name, tag string) (bool, error)
|
||||
// Bootstrap is the bootstrap's one-off credential for the holders' first images (design 51 *On a fresh mesh*):
|
||||
// read on each write, never kept, so the file's removal (BootstrapFile) refuses the next write that carries it.
|
||||
Bootstrap func() (token string, ok bool)
|
||||
Logf func(string, ...any)
|
||||
proxy *httputil.ReverseProxy
|
||||
}
|
||||
@@ -40,14 +37,7 @@ func (f *Front) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f.proxy.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
bootstrap := false
|
||||
if f.Bootstrap != nil {
|
||||
if token, ok := f.Bootstrap(); ok && token != "" {
|
||||
got, has := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
bootstrap = has && subtle.ConstantTimeCompare([]byte(got), []byte(token)) == 1
|
||||
}
|
||||
}
|
||||
if !bootstrap && (f.Window == nil || !f.Window.Load()) {
|
||||
if f.Window == nil || !f.Window.Load() {
|
||||
refuse(w, http.StatusForbidden, "DENIED", "the store takes writes from distribution's own process alone: publish through the seat's publish")
|
||||
return
|
||||
}
|
||||
@@ -62,21 +52,12 @@ func (f *Front) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if bootstrap {
|
||||
if f.Logf != nil {
|
||||
f.Logf("a write %s %s from %s with the bootstrap's one-off credential", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
}
|
||||
} else if f.Logf != nil {
|
||||
if f.Logf != nil {
|
||||
f.Logf("a direct write %s %s from %s, accepted while the direct-write window is open", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
}
|
||||
f.proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// BootstrapFile is where the node-engine's bootstrap gives distribution its one-off credential, in distribution's
|
||||
// state directory (0600, distribution's account): one line, the token. The tools bundle removes it when it answers its
|
||||
// first publish, so the holders serve publish and the credential is gone (RemoveBootstrap).
|
||||
const BootstrapFile = "/var/lib/mesh/registry/bootstrap-push"
|
||||
|
||||
// manifestPut is a PUT of /v2/<name>/manifests/<tag>, a tag and not a digest.
|
||||
func manifestPut(r *http.Request) (name, tag string, ok bool) {
|
||||
if r.Method != http.MethodPut {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
type memObjects map[string][]byte
|
||||
@@ -89,6 +91,7 @@ type fakeCatalog struct {
|
||||
tags map[string]string // name:version -> digest
|
||||
removed []string
|
||||
mirrorErr error
|
||||
tagErr error
|
||||
indexes map[string][]string // name@digest -> the platform manifests it names
|
||||
}
|
||||
|
||||
@@ -104,11 +107,27 @@ func (c *fakeCatalog) Versions(name string) ([]string, error) {
|
||||
func (c *fakeCatalog) Digest(name, version string) (string, error) {
|
||||
return c.tags[name+":"+version], nil
|
||||
}
|
||||
|
||||
// Remove deletes by the digest, as the store does: every tag naming the digest goes with it.
|
||||
func (c *fakeCatalog) Remove(name, version string) error {
|
||||
c.removed = append(c.removed, name+":"+version)
|
||||
delete(c.tags, name+":"+version)
|
||||
d := c.tags[name+":"+version]
|
||||
for k, v := range c.tags {
|
||||
if n, _, _ := strings.Cut(k, ":"); n == name && v == d {
|
||||
delete(c.tags, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTag answers as OCI.RemoveTag over registry 2.8.3 does (fakeRegistry's DELETE by tag): the store cannot
|
||||
// remove a tag alone, and nothing goes. tagErr is any other failure of the store.
|
||||
func (c *fakeCatalog) RemoveTag(name, version string) error {
|
||||
if c.tagErr != nil {
|
||||
return c.tagErr
|
||||
}
|
||||
return fmt.Errorf("%w: DELETE of %s:%s by its tag answered 400 DIGEST_INVALID", ErrNoTagAlone, name, version)
|
||||
}
|
||||
func (c *fakeCatalog) RemoveDigest(name, digest string) error {
|
||||
c.removed = append(c.removed, name+"@"+digest)
|
||||
return nil
|
||||
@@ -143,6 +162,14 @@ func (m memMirrors) Claim(k string, r MirrorRequest, rev uint64) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (m memMirrors) Delete(k string) error { delete(m, k); return nil }
|
||||
func (m memMirrors) List() (map[string]MirrorRequest, error) {
|
||||
out := map[string]MirrorRequest{}
|
||||
for k, r := range m {
|
||||
out[k] = r
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (m memMirrors) Put(k string, r MirrorRequest) error { m[k] = r; return nil }
|
||||
|
||||
// mirrorRev is a kept ask's revision in memMirrors: 1 unclaimed, 2 once claimed.
|
||||
func mirrorRev(r MirrorRequest) uint64 {
|
||||
@@ -178,6 +205,9 @@ func newBed(builderOK bool) *bed {
|
||||
return b
|
||||
}
|
||||
|
||||
// capOf is a binding's cap of n bytes.
|
||||
func capOf(n uint64) func() (uint64, error) { return func() (uint64, error) { return n, nil } }
|
||||
|
||||
func keys[V any](m map[string]V) []string {
|
||||
var out []string
|
||||
for k := range m {
|
||||
@@ -251,7 +281,11 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
_, err := b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t", Digest: part.Digest, Data: make([]byte, MaxPart+1)})
|
||||
return err
|
||||
}, "over the"},
|
||||
{"a new transfer at the cap", func(b *bed) error { b.h.Cap = 100; _, err := b.h.Publish(ArtifactSeat, builder, req("t")); return err }, "at their cap"},
|
||||
{"a new transfer at the cap", func(b *bed) error {
|
||||
b.h.Cap = capOf(100)
|
||||
_, err := b.h.Publish(ArtifactSeat, builder, req("t"))
|
||||
return err
|
||||
}, "at their cap"},
|
||||
{"a version the builder did not number", func(b *bed) error {
|
||||
b.h.Builder = fakeBuilder{ok: false}
|
||||
b.store.has[part.Digest] = true // held already: the publish ends at once
|
||||
@@ -265,14 +299,17 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
return err
|
||||
}, "holds bundles and archives"},
|
||||
{"remove of a version with no tag", func(b *bed) error {
|
||||
return b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Version: "0.1.4", Reason: "branch ended"})
|
||||
_, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Version: "0.1.4", Reason: "branch ended"})
|
||||
return err
|
||||
}, "has no tag"},
|
||||
{"a retention removal from another caller than the sweep", func(b *bed) error {
|
||||
return b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
|
||||
_, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/program", Digest: part.Digest, Reason: ReasonRetention})
|
||||
return err
|
||||
}, "artifact sweep's alone"},
|
||||
{"a retention removal of a held archive", func(b *bed) error {
|
||||
_ = b.v.HoldOne(ArtifactSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/a", Kind: KindArchive, Digest: part.Digest})
|
||||
return b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/a", Digest: part.Digest, Reason: ReasonRetention})
|
||||
_, err := b.v.Remove(ArtifactSeat, "novox.artifact-store-tools", RemoveRequest{Name: "m/a", Digest: part.Digest, Reason: ReasonRetention})
|
||||
return err
|
||||
}, "is held"},
|
||||
{"a hold of an image", func(b *bed) error {
|
||||
return b.v.HoldOne(ImageSeat, "novox.artifact-store-tools", HoldRequest{Name: "m/i", Kind: KindImage, Digest: part.Digest})
|
||||
@@ -316,10 +353,55 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
if dropped, _ := b.h.DropIdle(); !reflect.DeepEqual(dropped, []string{"t2"}) {
|
||||
t.Errorf("DropIdle = %v, want t2 after 24 hours with no part", dropped)
|
||||
}
|
||||
// Running review F2, ADR 0611 §3: the drop follows each transfer's idle key. The open arms it for 24 hours; its
|
||||
// end re-arms a transfer that took a part since for the rest, and drops one idle 24 hours, its chunks with it.
|
||||
b = newBed(true)
|
||||
armed := map[string]time.Duration{}
|
||||
b.h.Idle = func(id string, ttl time.Duration) error { armed[id] = ttl; return nil }
|
||||
if _, err := b.h.Publish(ArtifactSeat, builder, req("t4")); err != nil || armed["t4"] != TransferIdle {
|
||||
t.Fatalf("publish = %v, armed %v; want t4's idle key for %s", err, armed, TransferIdle)
|
||||
}
|
||||
b.now = b.now.Add(time.Hour)
|
||||
_, _ = b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t4", Digest: part.Digest, Data: blob[:100]})
|
||||
b.now = b.now.Add(TransferIdle - time.Hour)
|
||||
if dropped, err := b.h.Expired("t4"); err != nil || dropped || armed["t4"] != time.Hour {
|
||||
t.Errorf("the key's end an hour after a part: dropped %v, %v, armed %v; want t4 kept and armed for 1h", dropped, err, armed)
|
||||
}
|
||||
b.now = b.now.Add(time.Hour)
|
||||
if dropped, err := b.h.Expired("t4"); err != nil || !dropped {
|
||||
t.Errorf("the key's end 24 hours after the last part: dropped %v, %v; want t4 dropped", dropped, err)
|
||||
}
|
||||
if _, found, _ := b.h.Transfers.Get("t4"); found || len(b.h.Objects.(memObjects)) != 0 {
|
||||
t.Errorf("t4 after its drop: kept %v, objects %v; want both gone", found, b.h.Objects)
|
||||
}
|
||||
if dropped, err := b.h.Expired("t4"); err != nil || dropped {
|
||||
t.Errorf("the end of a key whose transfer is gone: %v, %v; want nothing done", dropped, err)
|
||||
}
|
||||
b.h.Idle = func(string, time.Duration) error { return errors.New("bus down") }
|
||||
if _, err := b.h.Publish(ArtifactSeat, builder, req("t5")); err == nil || !strings.Contains(err.Error(), "idle bound") {
|
||||
t.Errorf("a publish whose idle key is not written = %v; want it failed loudly", err)
|
||||
}
|
||||
// Seams note 2: registry_incoming's drop is a hand act recorded through mesh-hand-acts before anything goes;
|
||||
// an act not recorded leaves the transfer open.
|
||||
var acts []handacts.Request
|
||||
b.h.Record = func(q handacts.Request) error { return errors.New("mesh-hand-acts has no holder") }
|
||||
if _, err := b.h.DropByHand("t5", "person.jochen", "stuck"); err == nil || !strings.Contains(err.Error(), "stays open") {
|
||||
t.Errorf("a drop whose act is not recorded = %v; want refused, t5 open", err)
|
||||
}
|
||||
b.h.Record = func(q handacts.Request) error { acts = append(acts, q); return nil }
|
||||
if _, err := b.h.DropByHand("t5", "person.jochen", ""); err == nil || len(acts) != 0 {
|
||||
t.Errorf("a drop with no why = %v, acts %v; want refused, nothing recorded", err, acts)
|
||||
}
|
||||
if got, err := b.h.DropByHand("t5", "person.jochen", "stuck"); err != nil || got.ID != "t5" || len(acts) != 1 || acts[0].Why != "stuck" || acts[0].Cause != "registry_incoming" || !strings.Contains(acts[0].What, "t5") || !strings.Contains(acts[0].What, "person.jochen") {
|
||||
t.Errorf("a drop by hand = %+v, %v, acts %+v; want t5 dropped and its act recorded", got, err, acts)
|
||||
}
|
||||
if _, found, _ := b.h.Transfers.Get("t5"); found {
|
||||
t.Errorf("t5 kept after its drop by hand")
|
||||
}
|
||||
})
|
||||
t.Run("incoming's finding at 80 %, cleared below", func(t *testing.T) {
|
||||
b := newBed(true)
|
||||
b.h.Cap = 3500
|
||||
b.h.Cap = capOf(3500)
|
||||
_, _ = b.h.Publish(ArtifactSeat, builder, req("t3"))
|
||||
_, _ = b.h.PublishPart(ArtifactSeat, builder, PartRequest{Transfer: "t3", Digest: part.Digest, Data: blob[:2800]})
|
||||
if !b.findings["incoming-full incoming"] {
|
||||
@@ -330,12 +412,12 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Error("the finding stayed after the transfer was dropped")
|
||||
}
|
||||
// A refusal for want of room stays loud on incoming until the holder takes a publish again.
|
||||
b.h.Cap = 100
|
||||
b.h.Cap = capOf(100)
|
||||
_, _ = b.h.Publish(ArtifactSeat, builder, req("t4"))
|
||||
if !b.findings["publish-refused incoming"] {
|
||||
t.Errorf("a refusal at the cap raised %v, want publish-refused on incoming", b.findings)
|
||||
}
|
||||
b.h.Cap = 1 << 20
|
||||
b.h.Cap = capOf(1 << 20)
|
||||
b.store.has[part.Digest] = true
|
||||
if _, err := b.h.Publish(ArtifactSeat, builder, req("t5")); err != nil || b.findings["publish-refused incoming"] {
|
||||
t.Errorf("the next publish taken = %v, findings %v: want the refusal at the cap cleared", err, b.findings)
|
||||
@@ -344,7 +426,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Run("the sweep's removal, hold and release", func(t *testing.T) {
|
||||
b := newBed(true)
|
||||
sweep := "novox.artifact-store-tools"
|
||||
if err := b.v.Remove(ArtifactSeat, sweep, RemoveRequest{Name: "m/a", Digest: part.Digest, Kind: KindArchive, Reason: ReasonRetention}); err != nil {
|
||||
if _, err := b.v.Remove(ArtifactSeat, sweep, RemoveRequest{Name: "m/a", Digest: part.Digest, Kind: KindArchive, Reason: ReasonRetention}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.v.HoldOne(ArtifactSeat, sweep, HoldRequest{Name: "m/b", Kind: KindArchive, Digest: part.Digest}); err != nil {
|
||||
@@ -365,9 +447,50 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Errorf("said %v, want %v", events, want)
|
||||
}
|
||||
b.cat.tags["m/p:0.1.5-feat-x.1"] = part.Digest
|
||||
if err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-x.1", Reason: "branch merged"}); err != nil || b.cat.tags["m/p:0.1.5-feat-x.1"] != "" {
|
||||
if _, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-x.1", Reason: "branch merged"}); err != nil || b.cat.tags["m/p:0.1.5-feat-x.1"] != "" {
|
||||
t.Errorf("the builder's removal of a tagged version: %v", err)
|
||||
}
|
||||
// Security review F1 on registry 2.8.3: a reproducible build gives a branch's tagged version and the trunk's
|
||||
// numbered one one digest; the store cannot remove the tag alone, so the version is kept whole, the numbered one
|
||||
// stays pullable, and remove answers and says kept (shared-digest, the other tags): no refusal, so the builder
|
||||
// raises no retry.
|
||||
b.cat.tags["m/p:0.1.6-feat-y.1"], b.cat.tags["m/p:0.1.6"] = part.Digest, part.Digest
|
||||
got, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.6-feat-y.1", Reason: "branch merged"})
|
||||
if err != nil || got.Removed || !got.Kept || got.Reason != ReasonSharedDigest || !reflect.DeepEqual(got.Tags, []string{"0.1.6"}) {
|
||||
t.Fatalf("the removal of a tagged version sharing its digest = %+v, %v; want kept, shared-digest, tags [0.1.6], no error", got, err)
|
||||
}
|
||||
if b.cat.tags["m/p:0.1.6-feat-y.1"] != part.Digest || b.cat.tags["m/p:0.1.6"] != part.Digest {
|
||||
t.Errorf("tags after the tagged version's removal: %v; want both kept at %s", b.cat.tags, part.Digest)
|
||||
}
|
||||
if last := b.said[len(b.said)-1]; last.event != EventKept || last.body.Reason != ReasonSharedDigest || last.body.Digest != part.Digest || !reflect.DeepEqual(last.body.Tags, []string{"0.1.6"}) {
|
||||
t.Errorf("said %s %+v; want kept, shared-digest, its digest and the tags [0.1.6]", last.event, last.body)
|
||||
}
|
||||
if got, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.5-feat-v.1", Reason: "branch merged"}); err != nil || !got.Removed || got.Kept {
|
||||
t.Errorf("the removal of a version already gone = %+v, %v; want removed", got, err)
|
||||
}
|
||||
// Any other failure of the store removes nothing and is refused naming the shared digest.
|
||||
b.cat.tags["m/p:0.1.7-feat-z.1"], b.cat.tags["m/p:0.1.7"] = part.Digest, part.Digest
|
||||
b.cat.tagErr = errors.New("the store answered 500")
|
||||
said := len(b.said)
|
||||
if _, err := b.v.Remove(ArtifactSeat, "novox.mesh-delivery-builder", RemoveRequest{Name: "m/p", Version: "0.1.7-feat-z.1", Reason: "branch merged"}); err == nil || !strings.Contains(err.Error(), "shares its digest") || b.cat.tags["m/p:0.1.7"] != part.Digest || len(b.said) != said {
|
||||
t.Errorf("a store failing otherwise: %v, tags %v; want refused, 0.1.7 kept, nothing said", err, b.cat.tags)
|
||||
}
|
||||
b.cat.tagErr = nil
|
||||
// The store's own answers, as registry 2.8.3 gives them: DELETE by tag removes nothing (ErrNoTagAlone); by
|
||||
// digest the manifest goes with every tag naming it.
|
||||
r283 := newFakeRegistry()
|
||||
sts := httptest.NewServer(r283)
|
||||
defer sts.Close()
|
||||
u, _ := url.Parse(sts.URL)
|
||||
o := &OCI{Base: u}
|
||||
manifest := []byte(`{"schemaVersion":2}`)
|
||||
r283.manifests["m/p:0.1.8-feat-w.1"], r283.manifests["m/p:0.1.8"] = manifest, manifest
|
||||
if err := o.RemoveTag("m/p", "0.1.8-feat-w.1"); !errors.Is(err, ErrNoTagAlone) || r283.manifests["m/p:0.1.8-feat-w.1"] == nil {
|
||||
t.Errorf("RemoveTag on 2.8.3 = %v; want ErrNoTagAlone and the tag kept", err)
|
||||
}
|
||||
if err := o.Remove("m/p", "0.1.8"); err != nil || len(r283.manifests) != 0 {
|
||||
t.Errorf("Remove by digest on 2.8.3 = %v, %d manifests left; want both tags gone", err, len(r283.manifests))
|
||||
}
|
||||
})
|
||||
// CAT-AS1: what the sweep did over the store's door is the holder's: a removal for retention of an index removes
|
||||
// its platform manifests first, but those a tagged or held index of the repository names; a manifest such an index
|
||||
@@ -398,7 +521,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
err := b.v.Remove(ImageSeat, sweep, RemoveRequest{Name: name, Digest: c.remove, Reason: ReasonRetention})
|
||||
_, err := b.v.Remove(ImageSeat, sweep, RemoveRequest{Name: name, Digest: c.remove, Reason: ReasonRetention})
|
||||
var removed []string
|
||||
for _, r := range b.cat.removed {
|
||||
removed = append(removed, strings.TrimPrefix(r, name+"@"))
|
||||
@@ -437,9 +560,7 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
defer store.Close()
|
||||
u, _ := url.Parse(store.URL)
|
||||
var open atomic.Bool
|
||||
token, kept := "s3cret", true
|
||||
f := &Front{Store: u, Window: &open, Published: func(name, tag string) (bool, error) { return name == "m/i" && tag == "0.1.3", nil },
|
||||
Bootstrap: func() (string, bool) { return token, kept }}
|
||||
f := &Front{Store: u, Window: &open, Published: func(name, tag string) (bool, error) { return name == "m/i" && tag == "0.1.3", nil }}
|
||||
auth := ""
|
||||
do := func(method, path string) int {
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -468,24 +589,12 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Errorf("window open %v: %s %s = %d, want %d", c.open, c.method, c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
// The bootstrap's one-off credential writes with the window closed, never over a published tag, and not once
|
||||
// its file is gone; a wrong token writes nothing.
|
||||
// No credential opens the front (the lead's ENG-5 ruling): a write carrying a token, with the window closed, is
|
||||
// refused as any other.
|
||||
open.Store(false)
|
||||
for _, c := range []struct {
|
||||
auth string
|
||||
kept bool
|
||||
path string
|
||||
want int
|
||||
}{
|
||||
{"s3cret", true, "/v2/gitea/program/manifests/0.1.0", http.StatusCreated},
|
||||
{"s3cret", true, "/v2/m/i/manifests/0.1.3", http.StatusForbidden},
|
||||
{"wrong", true, "/v2/gitea/program/manifests/0.1.0", http.StatusForbidden},
|
||||
{"s3cret", false, "/v2/gitea/program/manifests/0.1.0", http.StatusForbidden},
|
||||
} {
|
||||
auth, kept = c.auth, c.kept
|
||||
if got := do(http.MethodPut, c.path); got != c.want {
|
||||
t.Errorf("bootstrap %q kept %v: PUT %s = %d, want %d", c.auth, c.kept, c.path, got, c.want)
|
||||
}
|
||||
auth = "s3cret"
|
||||
if got := do(http.MethodPut, "/v2/gitea/program/manifests/0.1.0"); got != http.StatusForbidden {
|
||||
t.Errorf("a write with a bearer token and the window closed = %d, want %d", got, http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
t.Run("mirror copies an outside image by its pinned digest, every platform, only the parts the store lacks", func(t *testing.T) {
|
||||
@@ -593,6 +702,24 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Fatalf("two followers on one ask said %+v (ask id %q), asks %v: want one copy, said once under the ask's id", b.said[copies:], ask.ID, asked)
|
||||
}
|
||||
b.said = b.said[:copies]
|
||||
// registry_mirrors (seams review note 1): a kept copy is read, asked again with its claim let go, and dropped
|
||||
// by hand, which says refused under the ask's id with who and why.
|
||||
_, _ = b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "nats", Digest: d})
|
||||
asked[key] = MirrorRequest{Source: asked[key].Source, Digest: d, ID: asked[key].ID, Claim: "cut-follower"}
|
||||
if now, err := b.v.MirrorsNow(); err != nil || now[key].Claim != "cut-follower" {
|
||||
t.Fatalf("MirrorsNow = %v, %v; want the claimed ask", now, err)
|
||||
}
|
||||
if r, err := b.v.MirrorAgain(key); err != nil || r.Claim != "" || asked[key].Claim != "" {
|
||||
t.Errorf("MirrorAgain = %+v, %v (kept %+v); want the claim let go", r, err, asked[key])
|
||||
}
|
||||
if _, err := b.v.MirrorDrop(key, "person.jochen", ""); err == nil {
|
||||
t.Error("a drop with no why is taken")
|
||||
}
|
||||
if _, err := b.v.MirrorDrop(key, "person.jochen", "upstream gone"); err != nil || len(asked) != 0 ||
|
||||
b.said[copies].event != EventRefused || b.said[copies].body.ID == "" || !strings.Contains(b.said[copies].body.Reason, "dropped by hand by person.jochen: upstream gone") {
|
||||
t.Errorf("MirrorDrop = %v, said %+v, asks %v; want refused said under the ask's id and the ask gone", err, b.said[copies:], asked)
|
||||
}
|
||||
b.said = b.said[:copies]
|
||||
b.cat.mirrorErr = errors.New("the upstream said 404")
|
||||
_, _ = b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "nats", Digest: d})
|
||||
if err := b.v.Mirrored(asked[key]); err != nil || b.said[1].event != EventRefused || !strings.Contains(b.said[1].body.Reason, "404") || len(asked) != 0 {
|
||||
@@ -612,20 +739,26 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
t.Errorf("direct writes held by %v, want g14", got)
|
||||
}
|
||||
t0 := time.Unix(0, 0)
|
||||
w, closed, _ := Window{Name: OldNameWindow}.Step([]string{"g14"}, t0)
|
||||
all := []string{"ace", "g14", "novox"}
|
||||
w, closed, _ := Window{Name: OldNameWindow}.Step(all, []string{"g14"}, t0)
|
||||
if closed != nil || !w.Open {
|
||||
t.Fatalf("Step = %+v, %+v", w, closed)
|
||||
}
|
||||
if _, _, overdue := w.Step([]string{"g14"}, t0.Add(WindowFindingAfter)); !reflect.DeepEqual(overdue, []string{"g14"}) {
|
||||
if _, _, overdue := w.Step(all, []string{"g14"}, t0.Add(WindowFindingAfter)); !reflect.DeepEqual(overdue, []string{"g14"}) {
|
||||
t.Errorf("seven days: overdue %v", overdue)
|
||||
}
|
||||
// Security review F3: a read naming no node (an empty node_haves) judges nothing; the window stays open.
|
||||
if same, shut, _ := w.Step(nil, nil, t0.Add(time.Minute)); shut != nil || !same.Open || !reflect.DeepEqual(same.Holding, []string{"g14"}) {
|
||||
t.Errorf("a read naming no node: %+v, closed %+v; want the window open as it was", same, shut)
|
||||
}
|
||||
w, _ = w.Drop("g14", "person.jochen", "retired", t0)
|
||||
w, closed, _ = w.Step(HoldingOldName(nodes, w.Dropped), t0.Add(time.Hour))
|
||||
w, closed, _ = w.Step(all, HoldingOldName(nodes, w.Dropped), t0.Add(time.Hour))
|
||||
if closed == nil || closed.Window != OldNameWindow || !reflect.DeepEqual(closed.LastHolding, []string{"g14"}) || len(closed.Dropped) != 1 {
|
||||
t.Errorf("closing = %+v", closed)
|
||||
}
|
||||
if _, again, _ := w.Step([]string{"g14"}, t0.Add(2*time.Hour)); again != nil {
|
||||
t.Error("a closed window closed again")
|
||||
// Once closed, it stays closed: a node holding it later opens nothing.
|
||||
if again, shut, _ := w.Step(all, []string{"g14"}, t0.Add(2*time.Hour)); shut != nil || again.Open {
|
||||
t.Errorf("a closed window stepped on a holder: %+v, closed %+v; want it closed, said once", again, shut)
|
||||
}
|
||||
})
|
||||
t.Run("the windows' driver: every node read on each wake, closed once, a paused build agent's same build counted", func(t *testing.T) {
|
||||
@@ -647,6 +780,17 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
if !reflect.DeepEqual(st.w[OldNameWindow].Holding, []string{"g14"}) || !reflect.DeepEqual(st.w[DirectWritesWindow].Holding, []string{"g14"}) {
|
||||
t.Errorf("windows = %+v, want g14 holding both (away: states nothing; runs the old build)", st.w)
|
||||
}
|
||||
// The window's record says why g14 holds it: its manifest-reads timed out, counted as stating nothing.
|
||||
if why := st.w[OldNameWindow].Why["g14"]; !strings.Contains(why, "did not answer") || !strings.Contains(why, "timeout") {
|
||||
t.Errorf("why g14 holds the old name = %q, want the timeout said", why)
|
||||
}
|
||||
// Security review F3: a wake whose node_haves names no node closes neither window.
|
||||
haves := del.haves
|
||||
del.haves = nil
|
||||
if _, err := d.Wake("delivered"); err != nil || !st.w[OldNameWindow].Open || !st.w[DirectWritesWindow].Open || len(closed) != 0 {
|
||||
t.Fatalf("a wake on an empty node_haves: %v, windows %+v, closed %v; want both open, none said", err, st.w, closed)
|
||||
}
|
||||
del.haves = haves
|
||||
now = now.Add(WindowFindingAfter)
|
||||
_, _ = d.Wake("delivered")
|
||||
if !found[FindingWindowHeld+" "+OldNameWindow+".g14"] {
|
||||
@@ -729,5 +873,28 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
|
||||
if store < 0 || program < 0 || store > program {
|
||||
t.Errorf("store at %d, program at %d: want the store listed first", store, program)
|
||||
}
|
||||
// Running review F2: incoming asks the bus 8 GiB, the most the objects template allows, equal to IncomingCap
|
||||
// (said only where no binding is read); transfers keeps for good, the holder dropping an idle one by its key
|
||||
// in idle, a state of keys with their own time to live.
|
||||
var req struct {
|
||||
Requires []struct {
|
||||
Name string `json:"name"`
|
||||
Options map[string]any `json:"options"`
|
||||
} `json:"requires"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &req)
|
||||
opts := map[string]map[string]any{}
|
||||
for _, r := range req.Requires {
|
||||
opts[r.Name] = r.Options
|
||||
}
|
||||
if mib, _ := opts[IncomingObjects]["max-mib"].(float64); uint64(mib)<<20 != IncomingCap {
|
||||
t.Errorf("incoming asks %v MiB, IncomingCap is %d bytes: want them equal", opts[IncomingObjects]["max-mib"], uint64(IncomingCap))
|
||||
}
|
||||
if _, ok := opts[TransfersState]["ttl-seconds"]; ok {
|
||||
t.Errorf("transfers has a lifetime %v: the bus would drop a transfer's key and leave its parts", opts[TransfersState])
|
||||
}
|
||||
if opts[IdleState]["key-ttl"] != true {
|
||||
t.Errorf("idle's options %v: want key-ttl, each transfer's key ending on its own", opts[IdleState])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import (
|
||||
)
|
||||
|
||||
// fakeRegistry is an OCI registry in memory, enough for a mirror's copy: blobs, uploads and manifests by
|
||||
// reference. With token set it answers 401 with a Bearer challenge until a request carries it.
|
||||
// reference. With token set it answers 401 with a Bearer challenge until a request carries it. A DELETE of a manifest
|
||||
// answers as registry 2.8.3, the store's pinned image, does (distribution v2.8.3 registry/handlers/manifests.go,
|
||||
// DeleteManifest): by digest it deletes the manifest and every tag naming it; by a tag it deletes nothing and answers
|
||||
// 400 DIGEST_INVALID, the check of the empty digest it deletes instead failing.
|
||||
type fakeRegistry struct {
|
||||
mu sync.Mutex
|
||||
blobs map[string][]byte // name@digest
|
||||
@@ -69,6 +72,27 @@ func (f *fakeRegistry) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case strings.Contains(p, "/manifests/"):
|
||||
i := strings.Index(p, "/manifests/")
|
||||
key := p[:i] + ":" + p[i+11:]
|
||||
if r.Method == http.MethodDelete {
|
||||
ref := p[i+11:]
|
||||
if !strings.HasPrefix(ref, "sha256:") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = io.WriteString(w, `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest did not match uploaded content"}]}`)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for k, b := range f.manifests {
|
||||
if strings.HasPrefix(k, p[:i]+":") && digestOf(b) == ref {
|
||||
delete(f.manifests, k)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPut {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
f.manifests[key] = b
|
||||
@@ -98,7 +122,10 @@ type fakeDelivery struct {
|
||||
func (f *fakeDelivery) Haves() ([]NodeHave, error) { return f.haves, nil }
|
||||
func (f *fakeDelivery) Reads(node string) (bool, []string, error) {
|
||||
s, ok := f.stated[node]
|
||||
return ok, s, nil
|
||||
if !ok {
|
||||
return false, nil, errors.New("nats: timeout")
|
||||
}
|
||||
return true, s, nil
|
||||
}
|
||||
|
||||
type memWindows struct {
|
||||
|
||||
@@ -3,6 +3,7 @@ package holder
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -253,7 +254,29 @@ func (o *OCI) Digest(name, version string) (string, error) {
|
||||
return resp.Header.Get("Docker-Content-Digest"), nil
|
||||
}
|
||||
|
||||
// Remove deletes the manifest name holds at version.
|
||||
// ErrNoTagAlone is a store that cannot delete a tag alone. registry 2.8.3, the store's pinned image, is one: its
|
||||
// DeleteManifest (distribution v2.8.3 registry/handlers/manifests.go) ignores a tag reference and deletes the empty
|
||||
// digest, whose check fails, answered 400 DIGEST_INVALID; registry 3 untags alone.
|
||||
var ErrNoTagAlone = errors.New("the store cannot remove a tag alone")
|
||||
|
||||
// RemoveTag deletes the tag version of name alone (the OCI distribution protocol's DELETE of a manifest by its tag);
|
||||
// the manifest stays under every other tag. A store that cannot delete a tag answers ErrNoTagAlone, any other failure
|
||||
// an error of its own; either removes nothing.
|
||||
func (o *OCI) RemoveTag(name, version string) error {
|
||||
resp, b, err := o.do(http.MethodDelete, "/v2/"+name+"/manifests/"+version, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == http.StatusBadRequest && bytes.Contains(b, []byte(`"DIGEST_INVALID"`)) {
|
||||
return fmt.Errorf("%w: DELETE of %s:%s by its tag answered 400 DIGEST_INVALID", ErrNoTagAlone, name, version)
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNotFound {
|
||||
return fmt.Errorf("the store did not delete the tag %s:%s alone (%d): %s", name, version, resp.StatusCode, b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove deletes the manifest name holds at version, by its digest: every tag naming it goes with it.
|
||||
func (o *OCI) Remove(name, version string) error {
|
||||
d, err := o.Digest(name, version)
|
||||
if err != nil || d == "" {
|
||||
|
||||
@@ -9,19 +9,26 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/parts"
|
||||
)
|
||||
|
||||
// The bounds of incoming (design 51 *Publishing*, ADR 0611): a part is at most 1 MiB, the store 20 GiB, a transfer
|
||||
// with no part for 24 hours is dropped, and past 80 % of the cap the holder writes a finding.
|
||||
// The bounds of incoming (design 51 *Publishing*, ADR 0611): a part is at most 1 MiB, a transfer with no part for 24
|
||||
// hours is dropped, and past 80 % of the cap the holder writes a finding. The cap is the one incoming's binding carries
|
||||
// (the bus's own bound of the object store, read through the runtime); IncomingCap is the manifest's 8 GiB, the most
|
||||
// the objects template allows, said only where no binding is read (a test), and the manifest's test holds the two equal.
|
||||
const (
|
||||
MaxPart = 1 << 20
|
||||
IncomingCap = 20 << 30
|
||||
IncomingCap = 8 << 30
|
||||
TransferIdle = 24 * time.Hour
|
||||
IncomingFullPct = 80
|
||||
// IncomingObjects and TransfersState are the object store and the state distribution asks as bus state.
|
||||
IncomingObjects = "incoming"
|
||||
TransfersState = "transfers"
|
||||
// IdleState is the state of one key per open transfer with its own time to live (a state declared key-ttl): the
|
||||
// bus's marker of its end is the event that drops a transfer left with no part for TransferIdle (ADR 0611 §3), never
|
||||
// a timer.
|
||||
IdleState = "idle"
|
||||
)
|
||||
|
||||
// The kinds a seat holds.
|
||||
@@ -97,6 +104,8 @@ type Event struct {
|
||||
// Reference is the seat's answer: `<registry>/<module>/<artifact>@sha256:<digest>` (ADR 0605 §9).
|
||||
Reference string `json:"reference,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// Tags are, for kept, the other tags still naming its digest.
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
// By is who asked a removal, hold or release: the caller the tool runner proved.
|
||||
By string `json:"by,omitempty"`
|
||||
// ID is the event's id on the bus when the outcome has one of its own (a mirror ask's): the same outcome said
|
||||
@@ -109,6 +118,7 @@ const (
|
||||
EventPublished = "published"
|
||||
EventRefused = "refused"
|
||||
EventRemoved = "removed"
|
||||
EventKept = "kept"
|
||||
EventHeld = "held"
|
||||
EventReleased = "released"
|
||||
)
|
||||
@@ -156,8 +166,14 @@ type Holder struct {
|
||||
Say Say
|
||||
Finding Finding
|
||||
Now func() time.Time
|
||||
// Cap is incoming's cap; IncomingCap when zero.
|
||||
Cap uint64
|
||||
// Cap reads incoming's cap from its binding (parts.BusCap); IncomingCap when nil or when the binding names none.
|
||||
Cap func() (uint64, error)
|
||||
// Record records a hand act through the seat mesh-hand-acts (handacts.Recorder); a drop by hand is refused
|
||||
// without it.
|
||||
Record func(handacts.Request) error
|
||||
// Idle writes a transfer's idle key with its own time to live, once (parts.BusArm over IdleState); the receiver
|
||||
// arms it on each open that leaves a transfer open. Nil arms nothing.
|
||||
Idle func(id string, ttl time.Duration) error
|
||||
}
|
||||
|
||||
func (h *Holder) now() time.Time {
|
||||
@@ -167,18 +183,14 @@ func (h *Holder) now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func (h *Holder) cap() uint64 {
|
||||
if h.Cap > 0 {
|
||||
return h.Cap
|
||||
}
|
||||
return IncomingCap
|
||||
}
|
||||
// CapNow is incoming's cap as its binding carries it.
|
||||
func (h *Holder) CapNow() uint64 { return h.receiver().CapNow() }
|
||||
|
||||
// receiver binds mesh-sdk's receiving side to distribution's incoming, its transfers state, the builder's check, its
|
||||
// store and its seats' events.
|
||||
func (h *Holder) receiver() *parts.Receiver[Opening] {
|
||||
return &parts.Receiver[Opening]{Objects: h.Objects, Transfers: h.Transfers, Cap: h.cap(), MaxChunk: MaxPart,
|
||||
Idle: TransferIdle, Full: IncomingFullPct, Now: h.now,
|
||||
return &parts.Receiver[Opening]{Objects: h.Objects, Transfers: h.Transfers, Cap: IncomingCap, Bound: h.Cap,
|
||||
MaxChunk: MaxPart, Idle: TransferIdle, Arm: h.Idle, Full: IncomingFullPct, Now: h.now,
|
||||
// Only the missing layers are sent: a part the store already holds under the name counts as held.
|
||||
Holds: func(t Transfer, digest string) bool {
|
||||
ok, err := h.Store.Has(t.Shape.Name, digest)
|
||||
@@ -283,10 +295,38 @@ func (h *Holder) eventOf(seat string, r PublishRequest, ref, reason string) Even
|
||||
// Drop drops a transfer by hand (registry_incoming) or because it was idle for TransferIdle: its chunks and its key.
|
||||
func (h *Holder) Drop(id string) (Transfer, error) { return h.receiver().Drop(id) }
|
||||
|
||||
// DropIdle drops every transfer with no part for TransferIdle, answering them; it runs on each publish's open and each
|
||||
// registry_incoming call, never on a timer (the object store's own lifetime of 24 hours drops the bytes too).
|
||||
// DropByHand is registry_incoming's drop: by a person, with why, recorded as a hand act through mesh-hand-acts before
|
||||
// anything goes; an act not recorded leaves the transfer open.
|
||||
func (h *Holder) DropByHand(id, by, why string) (Transfer, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return Transfer{}, errors.New("a drop is a hand act: say why")
|
||||
}
|
||||
if h.Record == nil {
|
||||
return Transfer{}, errors.New("no hand act can be recorded here, so nothing is dropped")
|
||||
}
|
||||
t, found, err := h.Transfers.Get(id)
|
||||
if err != nil {
|
||||
return Transfer{}, fmt.Errorf("transfer %s cannot be read, so nothing is dropped: %w", id, err)
|
||||
}
|
||||
if !found {
|
||||
return Transfer{}, fmt.Errorf("transfer %s is not open", id)
|
||||
}
|
||||
what := fmt.Sprintf("dropped the open transfer %s of incoming (%s@%s on %s, opened by %s, %d parts lacked), by %s", id, t.Shape.Name, t.Shape.Version, t.Shape.Seat, t.Opener, len(t.Lacks()), by)
|
||||
if err := h.Record(handacts.Request{What: what, Why: why, Cause: "registry_incoming"}); err != nil {
|
||||
return Transfer{}, fmt.Errorf("the hand act cannot be recorded, so transfer %s stays open: %w", id, err)
|
||||
}
|
||||
return h.Drop(id)
|
||||
}
|
||||
|
||||
// DropIdle drops every transfer with no part for TransferIdle, answering them: registry_incoming's look, by hand. The
|
||||
// drop itself follows each transfer's idle key (Expired); the objects template keeps for good, so nothing else does.
|
||||
func (h *Holder) DropIdle() ([]string, error) { return h.receiver().DropIdle() }
|
||||
|
||||
// Expired is told that a transfer's idle key ended (the bus's marker, parts.FollowIdle): the receiver drops a transfer
|
||||
// with no part for TransferIdle, its chunks with it, arms one that took a part since again for the rest, and leaves one
|
||||
// already gone. It answers whether it dropped the transfer.
|
||||
func (h *Holder) Expired(id string) (bool, error) { return h.receiver().Expired(id) }
|
||||
|
||||
func checkPublish(seat string, r PublishRequest) error {
|
||||
switch {
|
||||
case !slices.Contains(Seats, seat):
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,8 +19,12 @@ type Catalog interface {
|
||||
Versions(name string) ([]string, error)
|
||||
// Digest is the digest name holds at version, empty when none.
|
||||
Digest(name, version string) (string, error)
|
||||
// Remove deletes name at version (its manifest); the store's collector frees its blobs.
|
||||
// Remove deletes name at version by its manifest's digest, every tag naming that digest with it; the store's
|
||||
// collector frees its blobs.
|
||||
Remove(name, version string) error
|
||||
// RemoveTag deletes the tag version of name alone, the manifest staying under every other tag naming it; a store
|
||||
// that cannot delete a tag alone answers ErrNoTagAlone and removes nothing.
|
||||
RemoveTag(name, version string) error
|
||||
// RemoveDigest deletes an untagged manifest of name by its digest (retention).
|
||||
RemoveDigest(name, digest string) error
|
||||
// Platforms are the manifests an index of name names; none for a manifest that is not an index or is not held.
|
||||
@@ -42,6 +47,10 @@ type Mirrors interface {
|
||||
// writer wrote the key first.
|
||||
Claim(key string, r MirrorRequest, revision uint64) (bool, error)
|
||||
Delete(key string) error
|
||||
// List is every ask kept, by its key: what registry_mirrors reads.
|
||||
List() (map[string]MirrorRequest, error)
|
||||
// Put writes r under key whatever it held: an ask asked again by hand, its claim let go.
|
||||
Put(key string, r MirrorRequest) error
|
||||
}
|
||||
|
||||
// HoldState is distribution's own state holds (bus state, template state; ADR 0617 §2).
|
||||
@@ -114,48 +123,100 @@ type RemoveRequest struct {
|
||||
// The seat definitions name it (the removal reasons' enum), so the sweep and the holder share one value.
|
||||
const ReasonRetention = reg.RemoveInputReasonRetention
|
||||
|
||||
// Removal is what remove answers: Removed, or Kept with its reason and the other tags naming its digest.
|
||||
type Removal struct {
|
||||
Removed bool
|
||||
Kept bool
|
||||
Reason string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// ReasonSharedDigest is why a removal was kept: another tag names its digest, and the store cannot remove a tag alone.
|
||||
const ReasonSharedDigest = reg.RemoveAnswerReasonSharedDigest
|
||||
|
||||
// Remove is `remove`: the builder removes a tagged version; the artifact sweep's holder an untagged bundle, archive or
|
||||
// copied base with the reason retention, which a hold keeps. Every removal says `removed` with its reason.
|
||||
func (v *Verbs) Remove(seat, caller string, r RemoveRequest) error {
|
||||
func (v *Verbs) Remove(seat, caller string, r RemoveRequest) (Removal, error) {
|
||||
if err := v.Callers.Allowed(VerbRemove, caller); err != nil {
|
||||
return err
|
||||
return Removal{}, err
|
||||
}
|
||||
if strings.TrimSpace(r.Name) == "" || strings.TrimSpace(r.Reason) == "" {
|
||||
return errors.New("remove: a name and a reason are required")
|
||||
return Removal{}, errors.New("remove: a name and a reason are required")
|
||||
}
|
||||
ev := Event{Name: r.Name, Version: r.Version, Kind: r.Kind, Digest: r.Digest, Holder: Self, Reason: r.Reason, By: caller}
|
||||
switch {
|
||||
case r.Reason == ReasonRetention:
|
||||
if CallerModule(caller) != SweepHolder {
|
||||
return fmt.Errorf("remove: refused for %s: a removal for retention is the artifact sweep's alone", caller)
|
||||
return Removal{}, fmt.Errorf("remove: refused for %s: a removal for retention is the artifact sweep's alone", caller)
|
||||
}
|
||||
if r.Digest == "" {
|
||||
return errors.New("remove: a removal for retention names the digest")
|
||||
return Removal{}, errors.New("remove: a removal for retention names the digest")
|
||||
}
|
||||
if _, held, err := v.Holds.Get(Hold{Seat: seat, Name: r.Name, Digest: r.Digest}.Key()); err != nil {
|
||||
return fmt.Errorf("remove: the holds cannot be read, so nothing is removed: %w", err)
|
||||
return Removal{}, fmt.Errorf("remove: the holds cannot be read, so nothing is removed: %w", err)
|
||||
} else if held {
|
||||
return fmt.Errorf("remove: refused: %s@%s is held; release it first", r.Name, r.Digest)
|
||||
return Removal{}, fmt.Errorf("remove: refused: %s@%s is held; release it first", r.Name, r.Digest)
|
||||
}
|
||||
if err := v.removeForRetention(seat, r.Name, r.Digest); err != nil {
|
||||
return err
|
||||
return Removal{}, err
|
||||
}
|
||||
default:
|
||||
if r.Version == "" || !strings.Contains(r.Version, "-") {
|
||||
return fmt.Errorf("remove: refused: %s@%s has no tag; only a tagged version is removed, but for retention", r.Name, r.Version)
|
||||
return Removal{}, fmt.Errorf("remove: refused: %s@%s has no tag; only a tagged version is removed, but for retention", r.Name, r.Version)
|
||||
}
|
||||
d, err := v.Catalog.Digest(r.Name, r.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove: the store cannot read %s@%s: %w", r.Name, r.Version, err)
|
||||
return Removal{}, fmt.Errorf("remove: the store cannot read %s@%s: %w", r.Name, r.Version, err)
|
||||
}
|
||||
if d != "" {
|
||||
if err := v.Catalog.Remove(r.Name, r.Version); err != nil {
|
||||
return fmt.Errorf("remove: the store did not remove %s@%s: %w", r.Name, r.Version, err)
|
||||
// Reproducible builds share a digest between a branch's tagged version and the trunk's numbered one: a
|
||||
// manifest deleted by its digest would take the trunk's version with it (security review F1). When another
|
||||
// tag names the digest, the tag alone goes. A store that cannot remove a tag alone (registry 2.8.3) keeps the
|
||||
// version whole, its tag and its bytes, which the other tags name anyway: remove answers and says kept with
|
||||
// the reason shared-digest and those tags, no refusal, so the builder raises no retry. Any other failure is
|
||||
// refused.
|
||||
others, err := v.otherTags(r.Name, r.Version, d)
|
||||
if err != nil {
|
||||
return Removal{}, fmt.Errorf("remove: the store cannot say which other tags of %s name %s, so nothing is removed: %w", r.Name, d, err)
|
||||
}
|
||||
if len(others) > 0 {
|
||||
if err := v.Catalog.RemoveTag(r.Name, r.Version); errors.Is(err, ErrNoTagAlone) {
|
||||
sort.Strings(others)
|
||||
ev.Digest, ev.Reason, ev.Tags = d, ReasonSharedDigest, others
|
||||
kept := Removal{Kept: true, Reason: ReasonSharedDigest, Tags: others}
|
||||
return kept, v.Say(seat, EventKept, ev)
|
||||
} else if err != nil {
|
||||
return Removal{}, fmt.Errorf("remove: refused: %s@%s shares its digest %s with %v, and the store did not remove the tag alone, so nothing is removed: %w", r.Name, r.Version, d, others, err)
|
||||
}
|
||||
} else if err := v.Catalog.Remove(r.Name, r.Version); err != nil {
|
||||
return Removal{}, fmt.Errorf("remove: the store did not remove %s@%s: %w", r.Name, r.Version, err)
|
||||
}
|
||||
}
|
||||
ev.Digest = d // a version already gone is said removed all the same, so the builder's retry ends
|
||||
}
|
||||
return v.Say(seat, EventRemoved, ev)
|
||||
return Removal{Removed: true}, v.Say(seat, EventRemoved, ev)
|
||||
}
|
||||
|
||||
// otherTags are the tags of name but version that name digest.
|
||||
func (v *Verbs) otherTags(name, version, digest string) ([]string, error) {
|
||||
tags, err := v.Catalog.Versions(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, t := range tags {
|
||||
if t == version {
|
||||
continue
|
||||
}
|
||||
d, err := v.Catalog.Digest(name, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d == digest {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HoldRequest is `hold` and `release`.
|
||||
@@ -316,6 +377,55 @@ func (v *Verbs) Take(key string, r MirrorRequest, revision uint64, current bool,
|
||||
return v.Mirrored(r)
|
||||
}
|
||||
|
||||
// MirrorsNow lists every copy `mirror` took on and has not ended, by its key, for registry_mirrors: what waits (no
|
||||
// claim) and what a follower took on (its claim).
|
||||
func (v *Verbs) MirrorsNow() (map[string]MirrorRequest, error) { return v.Mirrors.List() }
|
||||
|
||||
// MirrorAgain asks a kept copy again by hand (registry_mirrors, a hand act): its claim is let go, so the state's watch
|
||||
// hands it to a follower, which takes it on as a new ask. A copy whose follower was cut, or that a follower holds and
|
||||
// does not end, goes on this way without a restart.
|
||||
func (v *Verbs) MirrorAgain(key string) (MirrorRequest, error) {
|
||||
all, err := v.Mirrors.List()
|
||||
if err != nil {
|
||||
return MirrorRequest{}, fmt.Errorf("mirror: the asks cannot be read: %w", err)
|
||||
}
|
||||
r, ok := all[key]
|
||||
if !ok {
|
||||
return MirrorRequest{}, fmt.Errorf("mirror: no copy is kept under %s", key)
|
||||
}
|
||||
r.Claim = ""
|
||||
if err := v.Mirrors.Put(key, r); err != nil {
|
||||
return MirrorRequest{}, fmt.Errorf("mirror: %s is not asked again: %w", key, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// MirrorDrop drops a kept copy by hand (registry_mirrors, a hand act with who and why): it says refused on the image
|
||||
// seat's events with that reason, under the ask's id, so a waiter on the copy ends, and the ask leaves the state.
|
||||
func (v *Verbs) MirrorDrop(key, by, why string) (MirrorRequest, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return MirrorRequest{}, errors.New("mirror: a drop is a hand act: say why")
|
||||
}
|
||||
all, err := v.Mirrors.List()
|
||||
if err != nil {
|
||||
return MirrorRequest{}, fmt.Errorf("mirror: the asks cannot be read: %w", err)
|
||||
}
|
||||
r, ok := all[key]
|
||||
if !ok {
|
||||
return MirrorRequest{}, fmt.Errorf("mirror: no copy is kept under %s", key)
|
||||
}
|
||||
src, err := ParseSource(r.Source)
|
||||
if err != nil {
|
||||
return MirrorRequest{}, err
|
||||
}
|
||||
e := Event{Name: src.Name(), Kind: KindUpstream, Digest: r.Digest, Holder: Self, By: by, ID: r.ID,
|
||||
Reason: fmt.Sprintf("%s@%s was not copied: dropped by hand by %s: %s", r.Source, r.Digest, by, why)}
|
||||
if err := v.Say(ImageSeat, EventRefused, e); err != nil {
|
||||
return MirrorRequest{}, fmt.Errorf("mirror: the drop of %s cannot be said, so the ask stays: %w", key, err)
|
||||
}
|
||||
return r, v.Mirrors.Delete(key)
|
||||
}
|
||||
|
||||
// HeldNow lists the holds, for registry_holds.
|
||||
func (v *Verbs) HeldNow() ([]Hold, error) {
|
||||
all, err := v.Holds.List()
|
||||
|
||||
@@ -2,6 +2,8 @@ package holder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
@@ -16,10 +18,6 @@ import (
|
||||
const (
|
||||
OldNameWindow = "old-name"
|
||||
DirectWritesWindow = "direct-writes"
|
||||
// BootstrapWindow is the bootstrap's one-off credential (BootstrapFile): open while the file is there. The tools
|
||||
// bundle closes it at its first answered publish; the program, which runs as distribution's account and alone may
|
||||
// remove the file in distribution's state directory, removes it on that close.
|
||||
BootstrapWindow = "bootstrap-push"
|
||||
// WindowFindingAfter is how long a node holds a window open before it is distribution's finding naming the node.
|
||||
WindowFindingAfter = 7 * 24 * time.Hour
|
||||
// The events a window's close says on distribution's own family distribution.events (7 days, 16 MiB, drop
|
||||
@@ -31,16 +29,16 @@ const (
|
||||
)
|
||||
|
||||
// Window is one window's record, kept in distribution's state windows: what registry_old_name and
|
||||
// registry_direct_writes answer.
|
||||
// registry_direct_writes answer. Its open, holding, since, closed and last holding are mesh-sdk's delivery.WindowHeld,
|
||||
// the part every window keeper shares and steps by one rule (delivery.StepWindow).
|
||||
type Window struct {
|
||||
Name string `json:"name"`
|
||||
Open bool `json:"open"`
|
||||
Holding []string `json:"holding,omitempty"`
|
||||
Since map[string]time.Time `json:"since,omitempty"`
|
||||
Dropped []DroppedNode `json:"dropped,omitempty"`
|
||||
Closed time.Time `json:"closed,omitzero"`
|
||||
// LastHolding are the nodes that held it last, said when it closes.
|
||||
LastHolding []string `json:"last_holding,omitempty"`
|
||||
Name string `json:"name"`
|
||||
delivery.WindowHeld
|
||||
Dropped []DroppedNode `json:"dropped,omitempty"`
|
||||
// Why says, of each node holding the window, why it holds it: what its read answered, a node-engine whose
|
||||
// manifest-reads did not answer (an away node, an older node-engine, a timeout) included, so a timeout taken as
|
||||
// stating nothing is told apart from a node-engine that states the old names.
|
||||
Why map[string]string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// DroppedNode is one retired node dropped from a window by hand: a hand act.
|
||||
@@ -61,16 +59,42 @@ type Closed struct {
|
||||
}
|
||||
|
||||
// NodeRead is what distribution read of one node: the reader capabilities its node-engine states through
|
||||
// `manifest-reads` (Serves false while it does not serve the verb: it states nothing), whether its declaration names an
|
||||
// artifact, and its build modules' haves: whether each runs a build that publishes through the seats.
|
||||
// `manifest-reads` (Serves false while it does not serve the verb: it states nothing; NotServed then says what the
|
||||
// call answered), whether its declaration names an artifact, and its build modules' haves: whether each runs a build
|
||||
// that publishes through the seats.
|
||||
type NodeRead struct {
|
||||
Node string `json:"node"`
|
||||
Serves bool `json:"serves"`
|
||||
NotServed string `json:"not_served,omitempty"`
|
||||
Stated []string `json:"stated,omitempty"`
|
||||
NamesArtifact bool `json:"names_artifact"`
|
||||
BuildModules map[string]bool `json:"build_modules,omitempty"`
|
||||
}
|
||||
|
||||
// WhyOldName says why a node holds the old name open.
|
||||
func WhyOldName(n NodeRead) string {
|
||||
if !n.Serves {
|
||||
if n.NotServed != "" {
|
||||
return "its node-engine's manifest-reads did not answer, so it counts as stating nothing: " + n.NotServed
|
||||
}
|
||||
return "its node-engine does not serve manifest-reads, so it states nothing"
|
||||
}
|
||||
return "its node-engine does not state " + NewNamesCapability
|
||||
}
|
||||
|
||||
// WhyDirectWrites says why a node holds the direct writes open: its build modules running a build not heard
|
||||
// publishing through the seats.
|
||||
func WhyDirectWrites(n NodeRead) string {
|
||||
var old []string
|
||||
for m, publishes := range n.BuildModules {
|
||||
if !publishes {
|
||||
old = append(old, m)
|
||||
}
|
||||
}
|
||||
sort.Strings(old)
|
||||
return fmt.Sprintf("its %v run a build not heard publishing through the seats", old)
|
||||
}
|
||||
|
||||
// HoldingOldName are the nodes holding the old name open: a node whose declaration names an artifact and whose
|
||||
// node-engine does not state the new names (one not serving manifest-reads states nothing).
|
||||
func HoldingOldName(nodes []NodeRead, dropped []DroppedNode) []string {
|
||||
@@ -110,33 +134,25 @@ func isDropped(dropped []DroppedNode, node string) bool {
|
||||
return slices.ContainsFunc(dropped, func(d DroppedNode) bool { return d.Node == node })
|
||||
}
|
||||
|
||||
// Step moves the window to the nodes holding it at now. It answers the next record, the closing event when it closed in
|
||||
// this step (said once), and the nodes holding it past WindowFindingAfter (the finding; cleared when it closes). A
|
||||
// window once closed stays closed: a node that holds it later is no reason to serve the old name again.
|
||||
func (w Window) Step(holding []string, now time.Time) (next Window, closed *Closed, overdue []string) {
|
||||
next = Window{Name: w.Name, Dropped: w.Dropped, Closed: w.Closed, LastHolding: w.LastHolding}
|
||||
// Step moves the window by mesh-sdk's one rule (delivery.StepWindow): named is every node the read named, dropped ones
|
||||
// included, holding those holding the window at now. A read naming no node judges nothing. It answers the next record,
|
||||
// the closing event when it closed in this step (said once), and the nodes holding it past WindowFindingAfter (the
|
||||
// finding; cleared when it closes). A window once closed stays closed: a node that holds it later is no reason to serve
|
||||
// the old name again or to take direct writes again.
|
||||
func (w Window) Step(named, holding []string, now time.Time) (next Window, closed *Closed, overdue []string) {
|
||||
next = w
|
||||
if !w.Open && !w.Closed.IsZero() {
|
||||
return next, nil, nil
|
||||
}
|
||||
if len(holding) == 0 {
|
||||
next.Closed, next.LastHolding = now, w.Holding
|
||||
if next.LastHolding == nil {
|
||||
next.LastHolding = []string{}
|
||||
}
|
||||
return next, &Closed{Window: w.Name, At: now, LastHolding: next.LastHolding, Dropped: w.Dropped}, nil
|
||||
held, shut, overdue := delivery.StepWindow(w.WindowHeld, named, holding, now, WindowFindingAfter)
|
||||
next.WindowHeld = held
|
||||
if !held.Open {
|
||||
next.Why = nil
|
||||
}
|
||||
next.Open, next.Holding, next.Since = true, holding, map[string]time.Time{}
|
||||
for _, n := range holding {
|
||||
since, had := w.Since[n]
|
||||
if !had {
|
||||
since = now
|
||||
}
|
||||
next.Since[n] = since
|
||||
if now.Sub(since) >= WindowFindingAfter {
|
||||
overdue = append(overdue, n)
|
||||
}
|
||||
if !shut {
|
||||
return next, nil, overdue
|
||||
}
|
||||
return next, nil, overdue
|
||||
return next, &Closed{Window: w.Name, At: held.Closed, LastHolding: held.LastHolding, Dropped: w.Dropped}, nil
|
||||
}
|
||||
|
||||
// Drop drops a retired node from the window by hand, a hand act with who and why.
|
||||
|
||||
@@ -48,13 +48,15 @@
|
||||
"registry_health",
|
||||
"registry_holds",
|
||||
"registry_incoming",
|
||||
"registry_mirrors",
|
||||
"registry_old_name"
|
||||
],
|
||||
"invokes": [
|
||||
"distribution.registry_callers",
|
||||
"mesh-delivery-builder.builder_versions",
|
||||
"mesh-delivery-view.node_haves",
|
||||
"seat:node-engine.manifest-reads"
|
||||
"seat:node-engine.manifest-reads",
|
||||
"seat:mesh-hand-acts.hand-act"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-view.delivered",
|
||||
@@ -83,9 +85,14 @@
|
||||
{
|
||||
"name": "transfers",
|
||||
"storage": "key-value",
|
||||
"template": "state"
|
||||
},
|
||||
{
|
||||
"name": "idle",
|
||||
"storage": "key-value",
|
||||
"template": "state",
|
||||
"options": {
|
||||
"ttl-seconds": 86400
|
||||
"key-ttl": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,10 +2,10 @@ module dmenu
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module dnsmasq
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module docker-compose
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module docker
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module dunst
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module fail2ban
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@ module feh
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
+6
-4
@@ -1,7 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
|
||||
@@ -2,10 +2,10 @@ module flatpak
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/nats-io/nats.go v1.54.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user