Add mesh-hand-acts, holder of the seat mesh-hand-acts, so the hand-act log has a home the controller's lease does not hold (issue 1953)

Acts are kept 400 days under the runtime-proven caller and said as act-recorded; a warrant is read from the router's own record and recorded once per ask; hand-acts answers over both logs. The act layout is a copy of mesh-sdk's until L5-S2 lands.
This commit is contained in:
2026-10-29 21:54:21 +01:00
parent 9dd27d2edb
commit e1cdeebcdf
12 changed files with 1310 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/mesh-hand-acts
/cmd/mesh-hand-acts/mesh-hand-acts
+23
View File
@@ -0,0 +1,23 @@
# mesh-hand-acts
Holds the seat `mesh-hand-acts` (novox/hq ADR 0565 §10, §11): the hand-act log, moved out of the controller. Go, one
binary (`cmd/mesh-hand-acts`), served by a runtime of its own on its own bus account (`runs-as`), so that its account
alone writes the log.
## What it keeps
- `mesh-hand-acts_acts`: every act, one key `act-<ns>-<n>` (`warrant-<asker>-<ask>` for a warrant) in the layout of the
controller's log, kept 400 days by the bucket's age. Each act is said as `act-recorded`.
- `mesh-hand-acts_warrants`: one key `<asker>.<ask>` per warrant recorded, made once by create, naming its act.
## Its verbs
- `mesh-hand-acts.hand-act`: what, why, cause, the condition addressed, and the act's verb (a repair when not given;
`hand-act drill` for a drill, whose cause is always `drill`). Who did it is the caller the runtime proves.
- `mesh-hand-acts.warranted`: asker and ask. Read from the router's own record of that ask (`messenger.asks`), never
from the caller, and recorded once per ask; an ask still open, ended without a choice or another asker's is refused.
- `mesh-hand-acts.hand-acts`: both logs, this one and the controller's `mesh-controller_hand-acts` until its lease
ends, newest first, with the causes repaired by hand twice within a fortnight; `warrants=true` lists the warrants.
No act is written to both logs: every recorder calls this seat from its delivery, and the controller keeps its own
acts in its own bucket until it loses its lease.
@@ -0,0 +1,399 @@
package main
// The hand-act log (novox/hq ADR 0565 §10, §11): every act done by hand, with who, why and its cause, so a repair a
// person keeps making is found (S15) and a person's decision is read back. mesh-hand-acts holds the seat
// mesh-hand-acts, once for the mesh, and is the one writer of its state `acts` (each act a key that the bucket keeps
// 400 days) and `warrants` (one key `<asker>.<ask>` made once by create). Each act is said as `act-recorded`.
//
// **Who did it is the runtime's word, never the caller's**: `by` is the bus user the runtime proved. A warrant is the
// router's word, never the caller's: `warranted` reads the router's own record of the asker's ask and records who
// chose, through which channel, with which proofs and which answer, once per ask. The controller's bucket
// `mesh-controller_hand-acts` stays its own until it loses its lease and is read, never written: `hand-acts` answers
// over both logs, and no act is written to both (ADR 0565 §11).
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"sync/atomic"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Module and Seat are this module and the seat it holds once for the mesh.
const (
Module = "mesh-hand-acts"
Seat = "mesh-hand-acts"
)
// The states it keeps and reads, and the event it says.
const (
ActsState = "acts"
WarrantsState = "warrants"
// ControllerActs is the controller's log, read until its lease ends.
ControllerActs = "mesh-controller.hand-acts"
// RouterRecords is the router's record of every ask, `<asker>.<ask>` (ADR 0259): the warrant is read there.
RouterRecords = "messenger.asks"
RecordedEvent = "act-recorded"
)
// The verbs an act is recorded under, as the controller's log names them: what S15 reads to tell a repair from a
// person's decision by design.
const (
VerbRecord = "hand-act record"
VerbDrill = "hand-act drill"
VerbWarrant = "warrant"
// CauseDrill is a drill's cause, and the one cause a repair may not give.
CauseDrill = "drill"
// CauseOperatorAnswer is an act the operator chose on a warrant.
CauseOperatorAnswer = "operator-answer"
)
// RepeatedWithin is how far back a cause counts as repeated (S15).
const RepeatedWithin = 14 * 24 * time.Hour
// Act is one entry of the log, in the layout of the controller's (mesh-controller internal/link HandAct), so one
// reader reads both logs alike. **A copy of the layout mesh-sdk holds for an act (issue 1953, L5-S2)**, replaced by it
// when it lands.
type Act struct {
ID string `json:"id"`
At time.Time `json:"at"`
By string `json:"by"`
Verb string `json:"verb"`
Args []string `json:"arguments,omitempty"`
Why string `json:"why"`
Cause string `json:"cause"`
Condition string `json:"condition,omitempty"`
Kind string `json:"kind,omitempty"`
Carried []string `json:"carried,omitempty"`
Via string `json:"via,omitempty"`
Ask string `json:"ask,omitempty"`
Proofs []string `json:"proofs,omitempty"`
RequestedBy string `json:"requested-by,omitempty"`
Outcome string `json:"outcome,omitempty"`
// Log is which log holds it, set when it is read: this seat's, or the controller's. Never written.
Log string `json:"log,omitempty"`
}
// Store is the bus's buckets as this module reaches them, through its runtime. Create writes a key only when it has
// none, and says ErrExists otherwise.
type Store interface {
Get(state, key string) ([]byte, bool, error)
Put(state, key string, value any) error
Create(state, key string, value any) error
Keys(state string) ([]string, error)
}
// ErrExists is a create of a key another writer made first.
var ErrExists = errors.New("the key was made by another writer first")
// Log is mesh-hand-acts: the log and what records into it.
type Log struct {
Store Store
Emit func(event string, body any) error
Now func() time.Time
Logf func(format string, args ...any)
}
var seq atomic.Uint64
// actKey is an act's key: its time and a sequence, so the keys list in order, as the controller's do.
func actKey(at time.Time) string {
return "act-" + strconv.FormatInt(at.UnixNano(), 10) + "-" + strconv.FormatUint(seq.Add(1), 10)
}
// Recorded is what a recorder is answered: the act as kept.
type Recorded struct {
Act Act `json:"act"`
Said string `json:"said"`
}
// HandAct records one act a caller says it did, by hand or on purpose: what, why, its cause and the condition it
// addresses. by is the caller the runtime proved. verb is the act's verb as the log names it: a repair by default, a
// drill with VerbDrill, or a recorder's own (`conditions silence`, `config set`), which S15 reads as it reads the
// controller's.
func (l *Log) HandAct(by, verb, what, why, cause, condition string) (Recorded, error) {
what, why, cause, verb = strings.TrimSpace(what), strings.TrimSpace(why), strings.TrimSpace(cause), strings.TrimSpace(verb)
switch {
case !proven(by):
return Recorded{}, fmt.Errorf("who acts is the caller the runtime proves, and it proved none (%q): nothing was recorded", by)
case what == "":
return Recorded{}, errors.New("a hand act says what was done: nothing was recorded")
case why == "":
return Recorded{}, errors.New("a hand act says why: nothing was recorded")
}
if verb == "" {
verb = VerbRecord
}
if verb == VerbDrill {
cause = CauseDrill
}
switch {
case cause == "":
return Recorded{}, errors.New("a hand act says its cause, in a word or a condition's kind: the word a second act " +
"for the same reason uses, which is how a repair done twice is found. Nothing was recorded")
case strings.EqualFold(cause, CauseDrill) && verb != VerbDrill:
return Recorded{}, errors.New("a drill is not recorded as a repair: record it with verb " + VerbDrill +
", which no healer is wanted for. Nothing was recorded")
}
at := l.Now().UTC()
act := Act{ID: actKey(at), At: at, By: by, Verb: verb, Args: []string{what}, Why: why, Cause: cause,
Condition: strings.TrimSpace(condition)}
if err := l.record(act); err != nil {
return Recorded{}, err
}
return Recorded{Act: act, Said: fmt.Sprintf("recorded as %s: %s did %q, because %q (cause: %s)", act.ID, by, what,
why, cause)}, nil
}
// record writes one act and says it. An act not said is still kept, and the log says so.
func (l *Log) record(act Act) error {
if err := l.Store.Create(ActsState, act.ID, act); err != nil {
return fmt.Errorf("the act could not be recorded: %w", err)
}
if l.Emit != nil {
if err := l.Emit(RecordedEvent, act); err != nil {
l.Logf("[%s] %s is recorded and could NOT be said as %s: %v", Module, act.ID, RecordedEvent, err)
}
}
return nil
}
// routerRecord is what warranted reads of the router's record of an ask.
type routerRecord struct {
State string `json:"state"`
Warrant *asks.Warrant `json:"warrant"`
Ask map[string]any `json:"ask"`
}
var askerModule = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
// Warrant is one ask's warrant as recorded: the key `<asker>.<ask>` of `warrants`, naming the act it recorded.
type Warrant struct {
Act string `json:"act"`
At time.Time `json:"at"`
By string `json:"recorded-at-the-word-of"`
}
// Warranted records an asker's act on the operator's warrant for its ask, read from the router's record, once per ask
// however often it is asked (ADR 0274, ADR 0259). The caller gives which ask and nothing else.
func (l *Log) Warranted(caller, asker, ask string) (Recorded, error) {
switch {
case !proven(caller):
return Recorded{}, fmt.Errorf("who asks is the caller the runtime proves, and it proved none (%q): nothing was recorded", caller)
case !askerModule.MatchString(asker):
return Recorded{}, fmt.Errorf("%q is not a module's name", asker)
case !asks.UsableID(ask):
return Recorded{}, fmt.Errorf("%q is not an ask's id", ask)
}
key := asker + "." + ask
if raw, found, err := l.Store.Get(WarrantsState, key); err != nil {
return Recorded{}, err
} else if found {
return l.already(key, raw)
}
raw, found, err := l.Store.Get(RouterRecords, key)
if err != nil {
return Recorded{}, fmt.Errorf("the router's record of %s's ask %s cannot be read: %w", asker, ask, err)
}
var rec routerRecord
if found {
if err := json.Unmarshal(raw, &rec); err != nil {
return Recorded{}, fmt.Errorf("the router's record of %s's ask %s is not readable: %w", asker, ask, err)
}
}
w := rec.Warrant
switch {
case rec.State == "open" || rec.State == "held":
return Recorded{}, fmt.Errorf("%s's ask %s is still %s: nobody has answered it. Nothing was recorded", asker, ask, rec.State)
case !found || rec.State == "" || w == nil:
return Recorded{}, fmt.Errorf("the router holds no closed record of %s's ask %s. Nothing was recorded", asker, ask)
case w.Asker != asker || w.Ask != ask:
return Recorded{}, fmt.Errorf("the router's record is for %s's ask %s. Nothing was recorded", w.Asker, w.Ask)
case w.Outcome != asks.OutcomeChosen || w.By == nil:
return Recorded{}, fmt.Errorf("%s's ask %s ended %s: no person chose, so there is no warrant to record. Nothing was "+
"recorded", asker, ask, w.Outcome)
case w.AskDigest == "":
return Recorded{}, fmt.Errorf("the router's warrant for %s's ask %s names no ask digest. Nothing was recorded", asker, ask)
}
at := l.Now().UTC()
act := Act{ID: "warrant-" + asker + "-" + ask, At: w.At.UTC(), Verb: VerbWarrant,
Args: []string{fmt.Sprintf("the operator chose %s on %s's ask %s", w.Label, asker, ask)},
Why: fmt.Sprintf("%s (ask %s of %s)", w.Says(), ask, asker),
By: fmt.Sprintf("the %s, as %s identity %s", w.By.Who, w.By.Kind, w.By.Identity),
Cause: CauseOperatorAnswer, Via: via(*w), Ask: ask, Proofs: w.Proofs,
RequestedBy: asker + ", recorded at the word of " + caller,
Outcome: "chosen; what " + asker + " did with it is in its own record"}
if err := l.Store.Create(WarrantsState, key, Warrant{Act: act.ID, At: at, By: caller}); errors.Is(err, ErrExists) {
raw, _, gerr := l.Store.Get(WarrantsState, key)
if gerr != nil {
return Recorded{}, gerr
}
return l.already(key, raw)
} else if err != nil {
return Recorded{}, fmt.Errorf("the warrant could not be recorded: %w", err)
}
if err := l.record(act); err != nil && !errors.Is(err, ErrExists) {
return Recorded{}, err
}
return Recorded{Act: act, Said: fmt.Sprintf("recorded as %s: %s, through %s", act.ID, act.Why, act.Via)}, nil
}
// already answers a warrant recorded before; an act lost between the warrant and its act is written now.
func (l *Log) already(key string, raw []byte) (Recorded, error) {
var w Warrant
if err := json.Unmarshal(raw, &w); err != nil {
return Recorded{}, fmt.Errorf("the warrant %s is not readable: %w", key, err)
}
actRaw, found, err := l.Store.Get(ActsState, w.Act)
if err != nil {
return Recorded{}, err
}
if !found {
return Recorded{}, fmt.Errorf("the warrant %s names the act %s, which is not kept: asked again, it is read from "+
"the router again", key, w.Act)
}
var act Act
_ = json.Unmarshal(actRaw, &act)
return Recorded{Act: act, Said: "already recorded as " + act.ID + ": " + act.Why}, nil
}
func via(w asks.Warrant) string {
if w.By == nil {
return w.Channel
}
v := w.Channel + " (" + w.By.Kind + ")"
if w.By.Verified != "" {
v += ", " + w.By.Verified
}
return v
}
// proven is a bus user the runtime proved: never empty, never the SDK's unproven word.
func proven(by string) bool {
by = strings.TrimSpace(by)
return by != "" && !strings.EqualFold(by, "unproven") && !strings.HasPrefix(by, "(")
}
// Listing is what hand-acts answers: every act of both logs within the days asked, newest first, narrowed by cause
// and who, and the causes repaired by hand twice or more within a fortnight.
type Listing struct {
Acts []Act `json:"acts"`
Repeated map[string]int `json:"repeated"`
Days int `json:"days"`
// Unread says which log could not be read, so what is missing is said and never read as nothing.
Unread map[string]string `json:"unread,omitempty"`
Kept map[string]int `json:"kept"`
}
// HandActs reads both logs.
func (l *Log) HandActs(days int, cause, who string) Listing {
now := l.Now()
since := now.Add(-time.Duration(days) * 24 * time.Hour)
out := Listing{Acts: []Act{}, Repeated: map[string]int{}, Days: days, Kept: map[string]int{}}
var all []Act
for _, log := range []string{ActsState, ControllerActs} {
acts, kept, err := l.read(log, now.Add(-max(time.Duration(days)*24*time.Hour, RepeatedWithin)))
if err != nil {
if out.Unread == nil {
out.Unread = map[string]string{}
}
out.Unread[log] = err.Error()
continue
}
out.Kept[log] = kept
all = append(all, acts...)
}
for _, a := range all {
if now.Sub(a.At) <= RepeatedWithin && !decision(a) {
out.Repeated[a.Cause]++
}
if a.At.Before(since) || (cause != "" && a.Cause != cause) || (who != "" && !strings.Contains(a.By, who)) {
continue
}
out.Acts = append(out.Acts, a)
}
for c, n := range out.Repeated {
if n < 2 {
delete(out.Repeated, c)
}
}
sort.SliceStable(out.Acts, func(i, j int) bool { return out.Acts[i].At.After(out.Acts[j].At) })
return out
}
// read is every act of one log since a moment, and how many the log keeps. A key names its time, so only the keys
// within are read whole.
func (l *Log) read(log string, since time.Time) ([]Act, int, error) {
keys, err := l.Store.Keys(log)
if err != nil {
return nil, 0, err
}
var out []Act
for _, k := range keys {
if at, ok := keyTime(k); ok && at.Before(since) {
continue
}
raw, found, err := l.Store.Get(log, k)
if err != nil {
return nil, len(keys), err
}
if !found {
continue
}
var a Act
if json.Unmarshal(raw, &a) != nil || a.At.Before(since) {
continue
}
a.Log = log
if log == ActsState {
a.Log = Seat
}
out = append(out, a)
}
return out, len(keys), nil
}
// keyTime is the time an act's key names (`act-<ns>-<n>`), when it names one.
func keyTime(key string) (time.Time, bool) {
parts := strings.Split(key, "-")
if len(parts) != 3 || parts[0] != "act" {
return time.Time{}, false
}
ns, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return time.Time{}, false
}
return time.Unix(0, ns), true
}
// decided are the verbs whose acts are a person's decision by design, with the causes they hold for (every cause when
// none): the controller's handActVerbs as S15 reads them. A verb not listed is a repair.
var decided = map[string][]string{
VerbWarrant: nil, "seat": {"seat-passed"}, "conditions silence": {CauseOperatorAnswer}, VerbRecord: {CauseDrill},
"secret issued": nil, VerbDrill: nil, "retire approve": nil, "retire reject": nil, "cleanup delete": nil,
"dead-letters deliver": nil, "dead-letters drop": nil, "collect": nil, "mirrors": nil, "move-placement": nil,
"move-config": nil, "bus upgrade": nil, "upgrade release-backlog": nil, "secret accept": nil,
"secret rotate": {"leaked-in-logs"}, "rotate": {"leaked-in-logs"},
}
func decision(a Act) bool {
causes, ok := decided[a.Verb]
if !ok {
return false
}
if len(causes) == 0 {
return true
}
for _, c := range causes {
if c == a.Cause {
return true
}
}
return false
}
@@ -0,0 +1,213 @@
package main
import (
"encoding/json"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
)
type memStore struct {
mu sync.Mutex
kv map[string]map[string][]byte
keysErr map[string]error
}
func newStore() *memStore {
return &memStore{kv: map[string]map[string][]byte{}, keysErr: map[string]error{}}
}
func (s *memStore) Get(state, key string) ([]byte, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
v, ok := s.kv[state][key]
return v, ok, nil
}
func (s *memStore) put(state, key string, value any, create bool) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.kv[state] == nil {
s.kv[state] = map[string][]byte{}
}
if _, ok := s.kv[state][key]; ok && create {
return ErrExists
}
s.kv[state][key] = raw
return nil
}
func (s *memStore) Put(state, key string, value any) error { return s.put(state, key, value, false) }
func (s *memStore) Create(state, key string, value any) error { return s.put(state, key, value, true) }
func (s *memStore) Keys(state string) ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.keysErr[state]; err != nil {
return nil, err
}
var out []string
for k := range s.kv[state] {
out = append(out, k)
}
sort.Strings(out)
return out, nil
}
func newLog() (*Log, *memStore, *[]Act, *time.Time) {
store := newStore()
var said []Act
now := time.Date(2026, 10, 29, 22, 0, 0, 0, time.UTC)
l := &Log{Store: store, Logf: func(string, ...any) {}, Now: func() time.Time { return now },
Emit: func(event string, body any) error {
if a, ok := body.(Act); ok && event == RecordedEvent {
said = append(said, a)
}
return nil
}}
return l, store, &said, &now
}
// hand-act records what the proven caller says it did, once, and says it; it refuses what cannot be read back, a
// caller the runtime did not prove, and a drill recorded as a repair (ADR 0565 §10; issue 1953 L5-C1).
func TestHandActRecordsWhatTheProvenCallerDid(t *testing.T) {
cases := []struct {
name, by, verb, what, why, cause string
wantVerb, wantCause, err string
}{
{"a repair", "ace.mesh-config", "", "set dns", "it was wrong", "config", VerbRecord, "config", ""},
{"a recorder's own verb", "novox.mesh-conditions", "conditions silence", "silenced x", "known", "operator-answer",
"conditions silence", "operator-answer", ""},
{"a drill takes its cause", "novox.mesh-healers", VerbDrill, "stopped records", "does it raise", "", VerbDrill, CauseDrill, ""},
{"no proven caller", "unproven", "", "x", "y", "z", "", "", "proves"},
{"by said empty", "", "", "x", "y", "z", "", "", "proves"},
{"no why", "person.jochen", "", "x", "", "z", "", "", "says why"},
{"no what", "person.jochen", "", "", "y", "z", "", "", "says what"},
{"no cause", "person.jochen", "", "x", "y", "", "", "", "cause"},
{"a drill as a repair", "person.jochen", "", "x", "y", "drill", "", "", "not recorded as a repair"},
}
for _, tc := range cases {
l, store, said, _ := newLog()
r, err := l.HandAct(tc.by, tc.verb, tc.what, tc.why, tc.cause, "bus.x.missing")
keys, _ := store.Keys(ActsState)
if tc.err != "" {
if err == nil || !strings.Contains(err.Error(), tc.err) || len(keys) != 0 || len(*said) != 0 {
t.Errorf("%s: %v, kept %v", tc.name, err, keys)
}
continue
}
if err != nil || len(keys) != 1 || keys[0] != r.Act.ID || len(*said) != 1 {
t.Fatalf("%s: %v, kept %v, said %d", tc.name, err, keys, len(*said))
}
var kept Act
raw, _, _ := store.Get(ActsState, keys[0])
_ = json.Unmarshal(raw, &kept)
if kept.By != tc.by || kept.Verb != tc.wantVerb || kept.Cause != tc.wantCause || kept.Condition != "bus.x.missing" ||
kept.Args[0] != tc.what || kept.Log != "" {
t.Errorf("%s: kept %+v", tc.name, kept)
}
}
}
// warranted records the router's word once per ask, never the caller's; it refuses an ask still open, one ended
// without a choice, another asker's, and one the router holds no record of (ADR 0565 §10, ADR 0259).
func TestWarrantedRecordsTheRoutersWordOnce(t *testing.T) {
chosen := `{"state":"closed","warrant":{"ask":"a1","asker":"claude-code","outcome":"chosen","label":"Approve",
"by":{"who":"operator","kind":"telegram","identity":"42","verified":"P2"},"channel":"telegram","proofs":["P1","P2"],
"at":"2026-10-29T21:00:00Z","ask-digest":"sha256:x"}}`
cases := []struct {
name, record, asker, ask, err string
}{
{"chosen", chosen, "claude-code", "a1", ""},
{"still open", `{"state":"open"}`, "claude-code", "a1", "still open"},
{"no record", "", "claude-code", "a1", "no closed record"},
{"expired", `{"state":"closed","warrant":{"ask":"a1","asker":"claude-code","outcome":"expired","at":"2026-10-29T21:00:00Z"}}`,
"claude-code", "a1", "no person chose"},
{"another asker's", strings.Replace(chosen, `"asker":"claude-code"`, `"asker":"mesh-config"`, 1), "claude-code", "a1",
"record is for mesh-config"},
{"no digest", strings.Replace(chosen, `,"ask-digest":"sha256:x"`, "", 1), "claude-code", "a1", "no ask digest"},
{"not a module", chosen, "Claude Code", "a1", "not a module"},
}
for _, tc := range cases {
l, store, said, _ := newLog()
if tc.record != "" {
_ = store.Put(RouterRecords, "claude-code.a1", json.RawMessage(tc.record))
}
r, err := l.Warranted("ace.claude-code", tc.asker, tc.ask)
if tc.err != "" {
if keys, _ := store.Keys(ActsState); err == nil || !strings.Contains(err.Error(), tc.err) || len(keys) != 0 {
t.Errorf("%s: %v, kept %v", tc.name, err, keys)
}
continue
}
if err != nil || r.Act.Verb != VerbWarrant || r.Act.Cause != CauseOperatorAnswer ||
!strings.Contains(r.Act.By, "operator, as telegram identity 42") || r.Act.Via != "telegram (telegram), P2" ||
!strings.Contains(r.Act.RequestedBy, "ace.claude-code") || len(r.Act.Proofs) != 2 {
t.Fatalf("%s: %+v, %v", tc.name, r.Act, err)
}
// Asked again, by another instance: nothing more is recorded.
again, err := l.Warranted("g14.claude-code", "claude-code", "a1")
keys, _ := store.Keys(ActsState)
if err != nil || !strings.HasPrefix(again.Said, "already recorded") || len(keys) != 1 || len(*said) != 1 {
t.Errorf("%s: a second warranted: %q, %v, kept %v, said %d", tc.name, again.Said, err, keys, len(*said))
}
}
}
// hand-acts answers over both logs, newest first, narrowed by days, cause and who, and counts each cause repaired by
// hand twice in a fortnight across both, never a person's decision; a log it cannot read is said, never read as empty.
func TestHandActsReadsBothLogs(t *testing.T) {
l, store, _, now := newLog()
put := func(log, verb, cause, by string, ago time.Duration) {
at := now.Add(-ago)
_ = store.Put(log, actKey(at), Act{At: at, Verb: verb, Cause: cause, By: by, Why: "x"})
}
put(ActsState, VerbRecord, "relogin", "person.jochen", time.Hour)
put(ControllerActs, VerbRecord, "relogin", "shanks.claude-code", 3*24*time.Hour)
put(ControllerActs, VerbDrill, CauseDrill, "person.jochen", time.Hour)
put(ActsState, VerbDrill, CauseDrill, "person.jochen", 2*time.Hour)
put(ActsState, VerbWarrant, CauseOperatorAnswer, "the operator", time.Hour)
put(ActsState, VerbWarrant, CauseOperatorAnswer, "the operator", 2*time.Hour)
put(ControllerActs, VerbRecord, "config", "ace.mesh-config", 30*24*time.Hour)
cases := []struct {
name string
days int
cause, who string
want int
}{
{"a fortnight", 14, "", "", 6},
{"a day", 1, "", "", 5},
{"forty days", 40, "", "", 7},
{"by cause", 14, "relogin", "", 2},
{"by who", 14, "", "claude-code", 1},
}
for _, tc := range cases {
got := l.HandActs(tc.days, tc.cause, tc.who)
if len(got.Acts) != tc.want || len(got.Repeated) != 1 || got.Repeated["relogin"] != 2 || got.Unread != nil {
t.Errorf("%s: %d acts, repeated %v, unread %v", tc.name, len(got.Acts), got.Repeated, got.Unread)
}
for i := 1; i < len(got.Acts); i++ {
if got.Acts[i].At.After(got.Acts[i-1].At) {
t.Errorf("%s: not newest first", tc.name)
}
}
}
if got := l.HandActs(14, "", ""); got.Acts[0].Log != Seat || got.Kept[ControllerActs] != 3 {
t.Errorf("the log of an act or the kept counts are not said: %+v", got.Kept)
}
store.keysErr[ControllerActs] = errors.New("the bus is away")
if got := l.HandActs(14, "", ""); got.Unread[ControllerActs] == "" || len(got.Acts) != 4 {
t.Errorf("an unreadable log: %+v", got.Unread)
}
w, err := l.Warrants()
if err != nil || w.(map[string]any)["count"] != 0 {
t.Errorf("warrants answered %v, %v", w, err)
}
}
@@ -0,0 +1,51 @@
// mesh-hand-acts: the holder of the seat mesh-hand-acts (novox/hq ADR 0565 §10). A Go bundle served by a runtime of
// its own, on its own bus account (`runs-as`), so that its account alone writes the hand-act log (`acts`, each act kept
// 400 days) and `warrants`. It reads the router's record of each ask for a warrant and the controller's log for
// `hand-acts`. stdout is the MCP channel; what this module says, it says on stderr.
package main
import (
"errors"
"fmt"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
// runtimeStore is the bus, through the runtime that launched this bundle.
type runtimeStore struct{}
func (runtimeStore) Get(state, key string) ([]byte, bool, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
func (runtimeStore) Put(state, key string, value any) error {
_, err := stdio.State(state).Put(key, value)
return err
}
func (runtimeStore) Create(state, key string, value any) error {
_, err := stdio.State(state).Create(key, value)
if errors.Is(err, stdio.ErrChanged) {
return ErrExists
}
return err
}
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func main() {
l := &Log{Store: runtimeStore{}, Now: time.Now, Logf: logf,
Emit: func(event string, body any) error { return stdio.Emit(event, body) }}
if err := stdio.Serve("", Tools(l)); err != nil {
logf("%v", err)
os.Exit(1)
}
}
@@ -0,0 +1,65 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"slices"
"testing"
)
// The manifest says what the code does: it claims the seat mesh-hand-acts once for the mesh with its verbs (ADR 0565
// §1, §10), runs on its own account, says act-recorded, reads the controller's log and the router's records, and keeps
// acts and warrants 400 days. Every state it keeps and reads has a tool that reads it (ADR 0565 §15).
func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
Runs string `json:"runs-as"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
Emits []string `json:"emits"`
Reads []string `json:"reads"`
State []struct {
Name string `json:"name"`
TTL int `json:"ttl-seconds"`
} `json:"state"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Module != Module || m.Runs != Module {
t.Errorf("module %q runs as %q", m.Module, m.Runs)
}
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || m.Claims[0].Scope != "mesh" || !slices.Equal(m.Claims[0].Serves, Verbs) {
t.Errorf("claims %+v", m.Claims)
}
if !slices.Equal(m.Emits, []string{RecordedEvent}) || !slices.Equal(m.Reads, []string{ControllerActs, RouterRecords}) {
t.Errorf("emits %v, reads %v", m.Emits, m.Reads)
}
readBy := map[string]string{ActsState: HandActsTool, WarrantsState: HandActsTool, ControllerActs: HandActsTool,
RouterRecords: WarrantedTool}
tools := map[string]bool{}
for _, tool := range Tools(&Log{}) {
tools[tool.Name] = true
}
for _, s := range m.State {
if s.TTL != 400*24*60*60 {
t.Errorf("%s is kept %d s, want 400 days", s.Name, s.TTL)
}
if !tools[readBy[s.Name]] {
t.Errorf("the state %s has no tool that reads it", s.Name)
}
}
for _, r := range m.Reads {
if !tools[readBy[r]] {
t.Errorf("the read %s has no tool that answers it", r)
}
}
}
@@ -0,0 +1,2 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
modules/mesh-hand-acts/module.json
@@ -0,0 +1,110 @@
package main
// The seat's verbs (novox/hq ADR 0565 §10, §15): hand-act and warranted record, hand-acts reads both logs and, with
// warrants, every warrant recorded. A recovery is a call: a warrant whose act was lost is written again by asking
// warranted again, from the router's record.
import (
"encoding/json"
"fmt"
"sort"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Verbs are the seat's verbs, as the manifest claims them.
var Verbs = []string{"hand-act", "hand-acts", "warranted"}
// Tool names.
const (
HandActTool = Seat + ".hand-act"
HandActsTool = Seat + ".hand-acts"
WarrantedTool = Seat + ".warranted"
)
func obj(props map[string]any, required ...string) map[string]any {
m := map[string]any{"type": "object", "properties": props}
if len(required) > 0 {
m["required"] = required
}
return m
}
func str(d string) map[string]any { return map[string]any{"type": "string", "description": d} }
// Tools are what the runtime serves.
func Tools(l *Log) []stdio.Tool {
return []stdio.Tool{
{Name: HandActTool,
Description: "Record one act done by hand (novox/hq ADR 0565 §10): what was done, why, its cause in a word or a " +
"condition's kind (the word a second act for the same reason uses, which is how a repair done twice is " +
"found) and the condition it addresses. Who did it is the caller the runtime proves. verb is the act's " +
"verb as the log names it: a repair when not given, `hand-act drill` for a drill, or a recorder's own " +
"(`conditions silence`, `config set`). Said as act-recorded.",
Input: obj(map[string]any{"what": str("what was done"), "why": str("why, in a sentence"),
"cause": str("the cause, in a word or a condition's kind"), "condition": str("the condition's key, if any"),
"verb": str("the act's verb in the log; a repair when not given")}, "what", "why"),
Run: func(a map[string]any) (any, error) {
return l.HandAct(stdio.CallerOf(a), arg(a, "verb"), arg(a, "what"), arg(a, "why"), arg(a, "cause"), arg(a, "condition"))
}},
{Name: HandActsTool,
Description: "What was done by hand lately, over both logs (this seat's and the controller's until its lease " +
"ends), newest first: each act with who, why, its cause and which log holds it, and the causes repaired by " +
"hand twice or more within a fortnight (a healer is wanted). Narrowed by cause and who; warrants=true lists " +
"every warrant recorded instead.",
Input: obj(map[string]any{"days": map[string]any{"type": "number", "description": "how many days back (default 14)"},
"cause": str("only this cause"), "who": str("only acts whose who contains this"),
"warrants": str("\"true\": every warrant recorded, `<asker>.<ask>` with its act")}),
Run: func(a map[string]any) (any, error) {
if strings.EqualFold(arg(a, "warrants"), "true") || a["warrants"] == true {
return l.Warrants()
}
return l.HandActs(days(a), arg(a, "cause"), arg(a, "who")), nil
}},
{Name: WarrantedTool,
Description: "Record an asker's act on the operator's warrant (ADR 0274, ADR 0259): read from the router's own " +
"record of that ask — who chose, through which channel, with which proofs and which answer — never from the " +
"caller, once per ask; refused for an ask still open, ended without a choice, or another asker's.",
Input: obj(map[string]any{"asker": str("the module that asked"), "ask": str("its ask's id")}, "asker", "ask"),
Run: func(a map[string]any) (any, error) {
return l.Warranted(stdio.CallerOf(a), arg(a, "asker"), arg(a, "ask"))
}},
}
}
// Warrants is every warrant recorded, by key.
func (l *Log) Warrants() (any, error) {
keys, err := l.Store.Keys(WarrantsState)
if err != nil {
return nil, fmt.Errorf("the warrants cannot be read: %w", err)
}
sort.Strings(keys)
rows := map[string]json.RawMessage{}
for _, k := range keys {
if raw, found, err := l.Store.Get(WarrantsState, k); err == nil && found {
rows[k] = raw
}
}
return map[string]any{"warrants": rows, "count": len(rows)}, nil
}
func arg(a map[string]any, k string) string {
s, _ := a[k].(string)
return strings.TrimSpace(s)
}
func days(a map[string]any) int {
switch v := a["days"].(type) {
case float64:
if v > 0 {
return int(v)
}
case string:
var n int
if _, err := fmt.Sscan(v, &n); err == nil && n > 0 {
return n
}
}
return 14
}
@@ -0,0 +1,327 @@
//go:build integration
package main
// mesh-hand-acts whole against a real bus (novox/hq ADR 0565 §10; issue 1953 L5-C2): the built binary, played over its
// real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.* answered
// from a real nats-server's key-value buckets, mesh/publish onto its subjects). A recorder's hand act is kept in the
// log on the bus under the proven caller and heard as act-recorded; a warrant the router recorded is recorded once.
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/testbus"
)
type wire struct {
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
// player is the node's tool runtime, as far as the self-check needs it, over a real bus.
type player struct {
ctx context.Context
nc *nats.Conn
js jetstream.JetStream
mu sync.Mutex
in io.Writer
next int
pending map[string]chan wire
}
func (p *player) send(m map[string]any) {
raw, _ := json.Marshal(m)
p.mu.Lock()
defer p.mu.Unlock()
_, _ = p.in.Write(append(raw, '\n'))
}
func (p *player) answer(id json.RawMessage, result any, err error) {
if err != nil {
p.send(map[string]any{"id": id, "error": map[string]any{"code": -32000, "message": err.Error()}})
return
}
p.send(map[string]any{"id": id, "result": result})
}
func (p *player) serve(out io.Reader) {
scanner := bufio.NewScanner(out)
scanner.Buffer(make([]byte, 1<<20), 1<<24)
for scanner.Scan() {
var m wire
if json.Unmarshal(scanner.Bytes(), &m) != nil {
continue
}
if m.Method == "" {
var key string
_ = json.Unmarshal(m.ID, &key)
p.mu.Lock()
ch := p.pending[key]
delete(p.pending, key)
p.mu.Unlock()
if ch != nil {
ch <- m
}
continue
}
go p.handle(m)
}
}
// kv is the bucket a state names: this module's own by its local name, another's as `<owner>.<name>`.
func (p *player) kv(state string) (jetstream.KeyValue, error) {
if owner, name, ok := strings.Cut(state, "."); ok {
return p.js.KeyValue(p.ctx, owner+"_"+name)
}
return p.js.KeyValue(p.ctx, Module+"_"+state)
}
func (p *player) handle(m wire) {
var a struct {
State string `json:"state"`
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Body json.RawMessage `json:"body"`
}
_ = json.Unmarshal(m.Params, &a)
switch m.Method {
case "mesh/state.get":
kv, err := p.kv(a.State)
if err != nil {
p.answer(m.ID, nil, err)
return
}
e, err := kv.Get(p.ctx, a.Key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
p.answer(m.ID, nil, nil)
return
}
if err != nil {
p.answer(m.ID, nil, err)
return
}
p.answer(m.ID, map[string]any{"key": e.Key(), "value": json.RawMessage(e.Value()), "revision": e.Revision()}, nil)
case "mesh/state.put":
kv, err := p.kv(a.State)
if err == nil {
var rev uint64
if rev, err = kv.Put(p.ctx, a.Key, a.Value); err == nil {
p.answer(m.ID, map[string]any{"revision": rev}, nil)
return
}
}
p.answer(m.ID, nil, err)
case "mesh/state.create":
kv, err := p.kv(a.State)
if err == nil {
var rev uint64
if rev, err = kv.Create(p.ctx, a.Key, a.Value); err == nil {
p.answer(m.ID, map[string]any{"revision": rev}, nil)
return
}
if errors.Is(err, jetstream.ErrKeyExists) {
p.send(map[string]any{"id": m.ID, "error": map[string]any{"code": -32001, "message": "the key was written by another writer first"}})
return
}
}
p.answer(m.ID, nil, err)
case "mesh/state.watch":
kv, err := p.kv(a.State)
if err != nil {
p.answer(m.ID, nil, err)
return
}
key := a.Key
if key == "" {
key = ">"
}
w, err := kv.Watch(p.ctx, key)
if err != nil {
p.answer(m.ID, nil, err)
return
}
var watch string
_ = json.Unmarshal(m.ID, &watch)
current := true
for e := range w.Updates() {
if e == nil {
current = false
p.answer(m.ID, map[string]any{}, nil)
continue
}
op := "put"
if e.Operation() != jetstream.KeyValuePut {
op = "delete"
}
change := map[string]any{"state": a.State, "key": e.Key(), "op": op, "revision": e.Revision(), "current": current}
if op == "put" {
change["value"] = json.RawMessage(e.Value())
}
p.mu.Lock()
p.next++
id := fmt.Sprintf("w-%d", p.next)
p.mu.Unlock()
p.send(map[string]any{"id": id, "method": "mesh/state", "params": map[string]any{"watch": watch, "change": change}})
}
case "mesh/subscribe":
p.answer(m.ID, map[string]any{}, nil)
case "mesh/state.keys":
kv, err := p.kv(a.State)
if err != nil {
p.answer(m.ID, nil, err)
return
}
keys, err := kv.Keys(p.ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
keys, err = []string{}, nil
}
p.answer(m.ID, keys, err)
case "mesh/state.delete":
kv, err := p.kv(a.State)
if err == nil {
err = kv.Delete(p.ctx, a.Key)
}
p.answer(m.ID, map[string]any{}, err)
case "mesh/publish":
var e struct {
Key string `json:"key"`
Body json.RawMessage `json:"body"`
}
_ = json.Unmarshal(m.Params, &e)
p.answer(m.ID, map[string]any{}, p.nc.Publish("mesh.mod."+Module+".event."+e.Key, e.Body))
default:
p.answer(m.ID, nil, errors.New("nats: no responders available for request"))
}
}
func (p *player) call(name, caller string, args map[string]any) (json.RawMessage, error) {
p.mu.Lock()
p.next++
id := fmt.Sprintf("c-%d", p.next)
ch := make(chan wire, 1)
p.pending[id] = ch
p.mu.Unlock()
p.send(map[string]any{"id": id, "method": "tools/call", "params": map[string]any{"name": name, "arguments": args,
"_meta": map[string]any{"mesh/caller": caller}}})
select {
case m := <-ch:
if m.Error != nil {
return nil, errors.New(m.Error.Message)
}
var r struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
IsError bool `json:"isError"`
}
if err := json.Unmarshal(m.Result, &r); err != nil || len(r.Content) != 1 {
return nil, fmt.Errorf("%s answered %s", name, m.Result)
}
if r.IsError {
return nil, errors.New(r.Content[0].Text)
}
return json.RawMessage(r.Content[0].Text), nil
case <-time.After(60 * time.Second):
return nil, fmt.Errorf("%s did not answer in 60s", name)
}
}
// 1953/L5-C2: an act recorded through the seat is kept on the bus and heard as act-recorded.
func TestTheHandActLogWholeRecordsOnTheBus(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
dir := t.TempDir()
bin := filepath.Join(dir, Module)
build := exec.Command("go", "build", "-o", bin, ".")
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
t.Fatalf("mesh-hand-acts does not build: %v", err)
}
nc, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(nc.Close)
js, err := jetstream.New(nc)
if err != nil {
t.Fatal(err)
}
for _, b := range []string{Module + "_" + ActsState, Module + "_" + WarrantsState, "mesh-controller_hand-acts", "messenger_asks"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b}); err != nil {
t.Fatal(err)
}
}
recorded, err := nc.SubscribeSync("mesh.mod." + Module + ".event." + RecordedEvent)
if err != nil {
t.Fatal(err)
}
cmd := exec.CommandContext(ctx, bin)
cmd.Env = append(os.Environ(), "MESH_NODE=novox", "MESH_MODULE="+Module)
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = stdin.Close(); cancel(); _ = cmd.Wait() })
p := &player{ctx: ctx, nc: nc, js: js, in: stdin, pending: map[string]chan wire{}}
go p.serve(stdout)
raw, err := p.call(HandActTool, "ace.mesh-config", map[string]any{"what": "set dns on g14", "why": "it was wrong",
"cause": "config", "by": "someone else"})
if err != nil {
t.Fatal(err)
}
var r Recorded
if err := json.Unmarshal(raw, &r); err != nil || r.Act.By != "ace.mesh-config" {
t.Fatalf("hand-act answered %s", raw)
}
acts, _ := js.KeyValue(ctx, Module+"_"+ActsState)
e, err := acts.Get(ctx, r.Act.ID)
if err != nil || !strings.Contains(string(e.Value()), `"by":"ace.mesh-config"`) {
t.Fatalf("the act is not kept on the bus: %v", err)
}
msg, err := recorded.NextMsg(10 * time.Second)
if err != nil || !strings.Contains(string(msg.Data), r.Act.ID) {
t.Fatalf("act-recorded was not said: %v", err)
}
// A warrant the router recorded, asked twice: one act.
asks, _ := js.KeyValue(ctx, "messenger_asks")
if _, err := asks.Put(ctx, "claude-code.a1", []byte(`{"state":"closed","warrant":{"ask":"a1","asker":"claude-code",
"outcome":"chosen","label":"Approve","by":{"who":"operator","kind":"telegram","identity":"42"},"channel":"telegram",
"at":"2026-10-29T21:00:00Z","ask-digest":"sha256:x"}}`)); err != nil {
t.Fatal(err)
}
for _, caller := range []string{"ace.claude-code", "g14.claude-code"} {
if _, err := p.call(WarrantedTool, caller, map[string]any{"asker": "claude-code", "ask": "a1"}); err != nil {
t.Fatal(err)
}
}
if keys, _ := acts.Keys(ctx); len(keys) != 2 {
t.Errorf("the log keeps %v, want the act and one warrant", keys)
}
listed, err := p.call(HandActsTool, "person.jochen", map[string]any{})
if err != nil || !strings.Contains(string(listed), "warrant-claude-code-a1") || !strings.Contains(string(listed), r.Act.ID) {
t.Errorf("hand-acts answered %s, %v", listed, err)
}
}
+22
View File
@@ -0,0 +1,22 @@
module mesh-hand-acts
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba
github.com/nats-io/nats.go v1.54.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // 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
)
+27
View File
@@ -0,0 +1,27 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba h1:uXwkcQWgbu/UthizGHFaKPiA4NBVJgqL9q2jJnnGR1o=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba/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=
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/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/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=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
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.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=
+69
View File
@@ -0,0 +1,69 @@
{
"module": "mesh-hand-acts",
"version": "1",
"runs-as": "mesh-hand-acts",
"claims": [
{
"name": "mesh-hand-acts",
"scope": "mesh",
"serves": [
"hand-act",
"hand-acts",
"warranted"
]
}
],
"emits": [
"act-recorded"
],
"reads": [
"mesh-controller.hand-acts",
"messenger.asks"
],
"state": [
{
"name": "acts",
"ttl-seconds": 34560000
},
{
"name": "warrants",
"ttl-seconds": 34560000
}
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-hand-acts",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-hand-acts",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-hand-acts"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-hand-acts",
"place": ".",
"unknown": "program"
}
],
"build": {
"artifacts": [
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-hand-acts",
"binary": "mesh-hand-acts",
"loads": [
"mesh-hand-acts"
]
}
]
}
}