Files
mesh-catalog/modules/claude-code/cmd/claude-code/proposals.go
T
jochen 774f18afcf
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
claude-code: an ask taken after its refusal is still answered; the wait is bounded (issue 370 review)
A refusal heard on the decided subject is kept as heard, and a record of a person's choice found later is
still taken, so an ask the router's queue offered again is not lost in silence. A heard end must match the
proposal's ask; the wait stops reading at its deadline; the heard ends are forgotten when Propose returns.
2026-10-10 13:08:09 +02:00

1130 lines
45 KiB
Go

package main
// Shared instructions on the operator's approval (novox/hq ADR 0274, amending issue 340's decision 2).
//
// An agent may PROPOSE an instruction section for every Claude session on the mesh, or on some nodes: a rule,
// a hand-over, the context a later session needs. A proposal is text and nothing else. It is kept in the
// module's `proposals` state and asked of the operator through the operator channel (novox/hq ADR 0259) as an
// ask whose two answers, Approve and Decline, are both at the level approve, so only a channel that proves who
// answered (Telegram, today) can carry either. Nothing is active until the operator approves.
//
// **Every node proves the approval itself.** The `proposals` state is written by every node's tool runner, so a
// record there proves nothing. Each instance of this module, for each proposal it has not decided yet:
//
// 1. reads the router's own record of the ask, under this module's name (the bus lets only the router write it,
// 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
// 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
// the managed instruction file renders from. A proposal decided here is never decided again: a warrant heard
// twice, or replayed, changes nothing.
//
// Removing an approved section is a proposal too, approved the same way: a record deleted from the bus removes
// nothing, because the node's own file is what renders.
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
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.
const Module = "claude-code"
// ProposalsState is the module's state the proposals are kept in.
const ProposalsState = "proposals"
// MaxProposalBytes is the most one proposed text may hold: it is read by every session it reaches, every time.
const MaxProposalBytes = 16 << 10
// RecordFor and RecordEvery bound how long, and how often, a node asks the controller to record a warrant it
// decided while the controller has not taken it.
const (
RecordFor = 7 * 24 * time.Hour
RecordEvery = 10 * time.Minute
)
// RouterKeeps is how long the router keeps its record of an ask after it closed (novox/hq ADR 0259 §3): a node
// that has not read it by then cannot prove the answer.
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
// What a proposal does.
const (
ActionAdd = "add"
ActionReplace = "replace"
ActionRemove = "remove"
)
// The two answers, both at the level approve.
const (
OptionApprove = "approve"
OptionDecline = "decline"
)
// How much of the text the operator's phone is shown: all of it when it is short and every line may leave the
// mesh, else its first lines.
const (
WholeTextBytes = 2000
WholeTextLines = 40
FirstBytes = 1200
FirstLines = 20
)
// Proposal is one proposed change to the shared instructions, as the `proposals` state keeps it. Every field the
// ask is composed from is here, so any node composes the same ask from it.
type Proposal struct {
ID string `json:"id"`
Action string `json:"action"`
Scope string `json:"scope"`
Nodes []string `json:"nodes,omitempty"`
Name string `json:"name"`
Text string `json:"text,omitempty"`
Digest string `json:"digest"`
From string `json:"from"`
At time.Time `json:"at"`
// Asked is empty once the ask was published, or says why it was not; nothing composes from it.
Asked string `json:"asked,omitempty"`
}
// textDigest is the digest a proposal binds: SHA-256 over the text's bytes.
func textDigest(text string) string {
sum := sha256.Sum256([]byte(text))
return "sha256:" + hex.EncodeToString(sum[:])
}
// Fingerprint is the digest as the operator is shown it: its first 24 hexadecimal digits in groups of four, so
// no word of it is long enough for the router to take for a secret.
func (p Proposal) Fingerprint() string { return fingerprintOf(p.Digest) }
func fingerprintOf(digest string) string {
hexed := strings.TrimPrefix(digest, "sha256:")
if len(hexed) < 24 {
return hexed
}
var groups []string
for i := 0; i < 24; i += 4 {
groups = append(groups, hexed[i:i+4])
}
return strings.Join(groups, " ")
}
// Lines is how many lines the text has.
func (p Proposal) Lines() int {
if p.Text == "" {
return 0
}
return len(strings.Split(strings.TrimRight(p.Text, "\n"), "\n"))
}
// where says which sessions a proposal reaches, in words.
func (p Proposal) where() string {
if p.Scope == ScopeMesh {
return "every Claude session on the mesh"
}
return "every Claude session on " + joinAnd(p.Nodes)
}
func joinAnd(list []string) string {
switch len(list) {
case 0:
return ""
case 1:
return list[0]
}
return strings.Join(list[:len(list)-1], ", ") + " and " + list[len(list)-1]
}
// 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,
"scope": p.Scope, "nodes": strings.Join(p.Nodes, ","), "name": p.Name, "digest": p.Digest, "from": p.From,
"at": p.At.UTC().Format(time.RFC3339Nano)}
}
// 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.
func (p Proposal) Ask() asks.Ask {
verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action]
shown, whole := excerpt(p.Text)
part := ""
if p.Action != ActionRemove && !whole {
part = " Part shown"
}
headline := fmt.Sprintf("%s Claude instructions '%s'?%s", verb, p.Name, part)
if len([]rune(headline)) > asks.HeadlineLength {
headline = verb + " Claude instructions?" + part
}
var b strings.Builder
switch p.Action {
case ActionAdd:
fmt.Fprintf(&b, "Add instructions '%s' to %s?\n\n", p.Name, p.where())
case ActionReplace:
fmt.Fprintf(&b, "Replace the instructions '%s' in %s?\n\n", p.Name, p.where())
case ActionRemove:
fmt.Fprintf(&b, "Remove the instructions '%s' from %s?\n\n", p.Name, p.where())
}
fmt.Fprintf(&b, "An agent proposed this through the claude-code module on %s, at %s UTC.", p.From,
p.At.UTC().Format("15:04 on 2 Jan"))
if p.Action == ActionRemove {
fmt.Fprintf(&b, " The approved text it removes has the fingerprint %s.", p.Fingerprint())
} else {
fmt.Fprintf(&b, " The text has %d lines, %d bytes, fingerprint %s. Once approved, every new session there "+
"reads it as the mesh's own word until it is removed the same way.\n\n", p.Lines(), len(p.Text), p.Fingerprint())
if whole {
b.WriteString("The whole text:\n\n" + shown)
} else {
fmt.Fprintf(&b, "NOT SHOWN IN FULL here. Its first lines:\n\n%s\n\nRead all of it before you answer, at the controller's terminal: "+
"mesh-cli ask claude-code claude_code_proposals '{\"id\": \"%s\"}'. The fingerprint there must be %s.",
shown, p.ID, p.Fingerprint())
}
}
does := "the text becomes part of those sessions' instructions"
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",
About: "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name}
if len(p.Nodes) == 0 {
a.About = "instructions." + p.Scope + "." + p.Name
}
for _, o := range []struct{ id, label, does string }{
{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})
}
return a
}
// excerpt is what of a text the operator's phone is shown, and whether it is the whole text. A line that might
// carry what may not leave the mesh (a path, an address, a long unbroken word) is not shown, and a marker says
// so; the router refuses an ask whose words carry one, and the whole text is read at the terminal.
func excerpt(text string) (string, bool) {
lines := strings.Split(strings.TrimRight(text, "\n"), "\n")
whole := len(text) <= WholeTextBytes && len(lines) <= WholeTextLines
limitBytes, limitLines := FirstBytes, FirstLines
if whole {
limitBytes, limitLines = WholeTextBytes, WholeTextLines
}
var out []string
size, hidden := 0, 0
flush := func() {
if hidden > 0 {
out = append(out, fmt.Sprintf("[%d line(s) not shown here: a path, an address or a long word]", hidden))
hidden = 0
}
}
for i, line := range lines {
if i >= limitLines || size+len(line) > limitBytes {
whole = false
break
}
if !shownLine(line) {
whole = false
hidden++
continue
}
flush()
out = append(out, line)
size += len(line) + 1
}
flush()
return strings.Join(out, "\n"), whole
}
var (
unbroken = regexp.MustCompile(`[A-Za-z0-9+/=_]{20,}`)
hexRun = regexp.MustCompile(`(?i)[0-9a-f]{16,}`)
numbersAt = regexp.MustCompile(`\d+\.\d+\.\d+`)
hostLike = regexp.MustCompile(`(?i)\b[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,12}\b`)
assigned = regexp.MustCompile(`(?i)(password|passwd|passphrase|secret|token|api[_-]?key|credential|private[_-]?key)\s*[=:]`)
)
// shownLine says whether a line may be shown on the phone: no path, address, host-like name, long unbroken word
// or value given to a secret's name. Deliberately narrower than the router's own rule, so an ask is rarely
// refused for its words; a line kept back is read at the terminal.
func shownLine(line string) bool {
if strings.ContainsAny(line, "/\\@") || strings.Contains(line, "~") || strings.Contains(line, "$") {
return false
}
for _, re := range []*regexp.Regexp{unbroken, hexRun, numbersAt, assigned} {
if re.MatchString(line) {
return false
}
}
// A sentence's end ("word. Next") never matches, nor does "e.g."; anything else shaped like a.name is held back.
return !hostLike.MatchString(line)
}
// anyImport is an @ the agent's reader could take for a file to read in: one at a text's start or after anything that
// is not a letter or a digit — any whitespace, Unicode's included, or punctuation. Wider than the vendor's own
// pattern on purpose: an e-mail-like word is the one kind let through.
var anyImport = regexp.MustCompile(`(?:^|[^\p{L}\p{N}])@\S`)
// ownHeading is a heading of level one to three, the levels the managed instruction file's own sections use.
var ownHeading = regexp.MustCompile(`(?m)^[ \t]{0,3}#{1,3}([ \t]|$)[^\n]*`)
// setextHeading is a non-blank line underlined with = or -; htmlHeading an HTML heading of level one to three.
var (
setextHeading = regexp.MustCompile(`(?m)^[ \t]{0,3}\S[^\n]*\n[ \t]{0,3}(=+|-+)[ \t]*$`)
htmlHeading = regexp.MustCompile(`(?i)<h[1-3][\s>]`)
)
// invisible names the first character of a text a reader would not see and a session would read, or "": a
// control character but a line break and a tab, a format character (zero-width, bidirectional overrides, the
// tag block), private use, a noncharacter, and any space or separator but the plain space.
func invisible(text string) string {
for _, r := range text {
switch {
case r == '\n' || r == '\t' || r == ' ':
continue
case unicode.Is(unicode.Cc, r):
return fmt.Sprintf("a control character (U+%04X)", r)
case unicode.Is(unicode.Cf, r):
return fmt.Sprintf("an invisible format character (U+%04X)", r)
case (r >= 0xFE00 && r <= 0xFE0F) || (r >= 0xE0100 && r <= 0xE01EF):
return fmt.Sprintf("a variation selector (U+%04X)", r)
case r == 0x3164 || r == 0x115F || r == 0x1160 || r == 0xFFA0 || r == 0x2800:
return fmt.Sprintf("a blank letter (U+%04X)", r)
case unicode.Is(unicode.Co, r):
return fmt.Sprintf("a private-use character (U+%04X)", r)
case unicode.Is(unicode.Zs, r) || unicode.Is(unicode.Zl, r) || unicode.Is(unicode.Zp, r):
return fmt.Sprintf("a space or separator other than the plain space (U+%04X)", r)
case r == utf8.RuneError || (r >= 0xFDD0 && r <= 0xFDEF) || r&0xFFFE == 0xFFFE:
return fmt.Sprintf("a noncharacter (U+%04X)", r)
}
}
return ""
}
// ProposalProblem says why a proposal, as kept on the bus, is not one this module would have made, or "": every
// node judges it again before it applies an approval, because anything holding a tool runner's credential can
// write the record and ask for it under this module's name.
func ProposalProblem(p Proposal) string {
switch {
case !asks.UsableID(p.ID):
return "its id is not one"
case !itemName.MatchString(p.Name):
return "its name is not one"
case !nodeName.MatchString(p.From):
return "where it came from is not a node's name"
case p.At.IsZero():
return "it says no time"
case p.Scope == ScopeMesh && len(p.Nodes) > 0:
return "the mesh scope names no nodes"
case p.Scope == ScopeNode && len(p.Nodes) == 0:
return "the node scope names its nodes"
case p.Scope != ScopeMesh && p.Scope != ScopeNode:
return "its scope is neither mesh nor node"
}
if p.Scope == ScopeNode {
if problem := nodesProblem(p.Nodes); problem != "" {
return problem
}
for i := 1; i < len(p.Nodes); i++ {
if p.Nodes[i-1] >= p.Nodes[i] {
return "its nodes are not listed once each, in order"
}
}
}
switch p.Action {
case ActionAdd, ActionReplace:
if problem := ProposedTextProblem(p.Text); problem != "" {
return problem
}
if textDigest(p.Text) != p.Digest {
return "its text is not the text its digest names"
}
case ActionRemove:
if p.Text != "" || !strings.HasPrefix(p.Digest, "sha256:") {
return "a removal carries no text, and the digest of the text it removes"
}
default:
return "its action is not add, replace or remove"
}
return ""
}
// secretShaped are shapes a shared text never carries: it is read by every session and shown on a phone.
var secretShaped = []*regexp.Regexp{
regexp.MustCompile(`-----BEGIN [A-Z ]+-----`),
regexp.MustCompile(`\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}`),
regexp.MustCompile(`\b(gh[pousr]_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9_-]{16,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16})`),
regexp.MustCompile(`(?i)\b(password|passwd|passphrase|token|api[_-]?key|apikey|private[_-]?key)\s*[=:]\s*\S`),
}
// ProposedTextProblem says why a text cannot be proposed, or "": empty, too long, not text, or something a
// session would run or read in by itself (issue 340's refusals, kept): a shell line, front matter, a hook, a tool
// server, a permission mode or pre-approved tools, or an `@` reference, which the instruction file reads in as a
// file; or anything shaped like a secret.
func ProposedTextProblem(text string) string {
switch {
case strings.TrimSpace(text) == "":
return "the text is empty"
case len(text) > MaxProposalBytes:
return fmt.Sprintf("the text is %d bytes, more than the %d a shared section may hold: every session reads all of it", len(text), MaxProposalBytes)
case !utf8.ValidString(text) || strings.ContainsRune(text, 0):
return "the text is not text"
case invisible(text) != "":
return "the text holds " + invisible(text) + ", which a phone does not show and a session reads; a shared section is plain visible text"
case shellLine.MatchString(text):
return "the text holds a shell line (!`…` or ```!), which a session may run by itself; nothing proposed here runs (novox/hq issue 340)"
}
if keys, problem := frontMatterKeys(text); problem != "" || len(keys) > 0 {
for _, key := range keys {
norm := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(key))
if why, ok := runsFrontMatter[norm]; ok {
return fmt.Sprintf("the text's front matter says %q: %s; nothing proposed here runs or loosens what runs (novox/hq issue 340)", key, why)
}
}
return "the text opens with front matter, which a session may act on; a shared section is plain text (novox/hq issue 340)"
}
if m := anyImport.FindString(text); m != "" {
return fmt.Sprintf("the text holds %q: an @ after anything but a letter or a digit may be read in as a file, "+
"and a shared section refers to no file (novox/hq issue 340)", strings.TrimSpace(m))
}
if m := setextHeading.FindString(text); m != "" {
return "the text underlines a line with = or -, which makes it a heading that would read as a section of the mesh's own"
}
if m := htmlHeading.FindString(text); m != "" {
return fmt.Sprintf("the text holds %q, a heading that would read as a section of the mesh's own", m)
}
if m := ownHeading.FindString(text); m != "" {
return fmt.Sprintf("the text holds the heading %q, which would read as a section of the mesh's own; a shared "+
"section uses headings of level four or deeper", strings.TrimSpace(m))
}
for _, re := range secretShaped {
if re.MatchString(text) {
return "the text holds something shaped like a secret, which a shared section never carries"
}
}
return ""
}
// ---- what this node approved ------------------------------------------------------------------------
// ApprovedSection is one approved section as this node keeps it.
type ApprovedSection struct {
Name string `json:"name"`
Scope string `json:"scope"`
Nodes []string `json:"nodes,omitempty"`
Text string `json:"text"`
Digest string `json:"digest"`
Proposal string `json:"proposal"`
From string `json:"from"`
Approved time.Time `json:"approved"`
By string `json:"by"`
}
// ProposalDecision is what this node made of one proposal: once decided, never again.
type ProposalDecision struct {
Outcome string `json:"outcome"`
At time.Time `json:"at"`
Says string `json:"says,omitempty"`
// Recorded says the controller's hand-act log holds the warrant; Record is why not, while it does not.
// Warranted says a person's choice held here: Approve or Decline, recorded in the hand-act log.
Warranted bool `json:"warranted,omitempty"`
Recorded bool `json:"recorded,omitempty"`
// Heard says the decision came from the router's word on the decided subject, not its record: a record of a
// choice found later is still taken.
Heard bool `json:"heard,omitempty"`
Record string `json:"record,omitempty"`
LastTry time.Time `json:"last-try,omitempty"`
}
// Approved is this node's own record: the sections it renders, and every proposal it decided.
type Approved struct {
Sections map[string]ApprovedSection `json:"sections"`
Decided map[string]ProposalDecision `json:"decided"`
// Changed is, for each section's place, when the operator approved the last change applied to it here — an
// addition, a replacement or a removal. An approval older than it is never applied: an earlier approval
// replayed, or read late, does not undo a later one.
Changed map[string]time.Time `json:"changed,omitempty"`
// Refused is the proposals whose answer did not hold here, and why: asked again on every look, so an honest
// approval refused for a reason that passes (a node still on an older version) is taken when it holds.
Refused map[string]string `json:"refused,omitempty"`
}
func (p Paths) approved() string { return filepath.Join(p.State, "approved.json") }
// sectionKey is one section's place: its scope, the nodes it is for and its name, so a section for some nodes is
// never replaced or removed by a proposal for other nodes.
func sectionKey(scope string, nodes []string, name string) string {
return scope + "/" + strings.Join(nodes, ",") + "/" + name
}
// ReadApproved is this node's record, or an empty one when there is none yet. A record that cannot be read is
// an error, never taken for an empty one: that would forget every decision and open them all again.
func ReadApproved(p Paths) (Approved, error) {
var a Approved
raw, err := fsys.ReadFile(p.approved())
switch {
case errors.Is(err, os.ErrNotExist):
case err != nil:
return a, fmt.Errorf("this node's record of what the operator approved (%s) cannot be read: %w", p.approved(), err)
default:
if err := json.Unmarshal(raw, &a); err != nil {
return a, fmt.Errorf("this node's record of what the operator approved (%s) is not readable JSON: %w; "+
"nothing is decided or rendered from it until it is", p.approved(), err)
}
}
if a.Sections == nil {
a.Sections = map[string]ApprovedSection{}
}
if a.Decided == nil {
a.Decided = map[string]ProposalDecision{}
}
if a.Changed == nil {
a.Changed = map[string]time.Time{}
}
if a.Refused == nil {
a.Refused = map[string]string{}
}
return a, nil
}
func writeApproved(p Paths, a Approved) error {
raw, err := indented(a)
if err != nil {
return err
}
tmp := p.approved() + ".next"
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
if _, err := f.Write(raw); err != nil {
_ = f.Close()
return err
}
// On the disk before it takes the old one's place: a crash leaves the old record or the new, never an empty one.
if err := f.Sync(); err != nil {
_ = f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, p.approved()); err != nil {
return err
}
if dir, err := os.Open(p.State); err == nil {
_ = dir.Sync()
_ = dir.Close()
}
return nil
}
// ApprovedHere is the approved sections that reach this node, by scope then name.
func ApprovedHere(a Approved, node string) []ApprovedSection {
var out []ApprovedSection
for _, s := range a.Sections {
// Judged again as it renders: a record changed on disk renders nothing a proposal could not hold.
if !itemName.MatchString(s.Name) || !nodeName.MatchString(s.From) || ProposedTextProblem(s.Text) != "" ||
textDigest(s.Text) != s.Digest || (s.Scope != ScopeMesh && s.Scope != ScopeNode) ||
(s.Scope == ScopeNode && (len(s.Nodes) == 0 || nodesProblem(s.Nodes) != "")) {
continue
}
if s.Scope == ScopeMesh || contains(s.Nodes, node) {
out = append(out, s)
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].Scope != out[j].Scope {
return out[i].Scope < out[j].Scope
}
return out[i].Name < out[j].Name
})
return out
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
// ---- the bus, as proposals reach it -----------------------------------------------------------------
// AskBus is the operator channel as this module reaches it: publish an ask under its own name, read the router's
// record of one of its asks, and the kinds of the channel bench with what each promises.
type AskBus interface {
Publish(subject string, body any, id string) error
Record(id string) (*stdio.StateEntry, error)
Kinds() ([]stdio.KindHeld, error)
}
// ProposalStore is the `proposals` state.
type ProposalStore interface {
Create(key string, value any) error
Put(key string, value any) error
Keys() ([]string, error)
Get(key string) (json.RawMessage, bool, error)
}
// Recorder records a warrant in the controller's hand-act log: the controller reads the warrant from the
// router's own record, never from this module.
type Recorder func(ask string) error
type sdkAskBus struct{}
func (sdkAskBus) Publish(subject string, body any, id string) error {
return stdio.SeatPublish(subject, body, id)
}
func (sdkAskBus) Record(id string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", id) }
func (sdkAskBus) Kinds() ([]stdio.KindHeld, error) { return stdio.SeatKinds() }
// recordWarrant asks the controller to record a warrant this module acted on.
func recordWarrant(ask string) error {
_, err := stdio.Ask("seat:mesh-controller.warranted", map[string]any{"asker": Module, "ask": ask})
return err
}
// ---- proposing --------------------------------------------------------------------------------------
// ProposeInput is what an agent proposes.
type ProposeInput struct {
Scope string
Nodes []string
Name string
Text string
Remove bool
}
// AtTheTerminalInstead is the other way, said whenever a proposal is refused for want of a channel.
const AtTheTerminalInstead = "the operator may set it at the controller's terminal instead: mesh-cli settings set claude-code " +
"'{\"instructions\": {\"<name>\": [\"line\", …]}}' [--node <node>] (novox/hq issue 340)"
// approverReady says why no channel that proves who answered can carry an approval, when this module can tell:
// the channel bench's kinds, where its membership names them, and none promising a choice, an exact rendering and
// a verified sender. **The router is the judge** (novox/hq ADR 0259 §4): it refuses an ask none of its ready
// channels can carry, on the decided subject, and Propose waits for that word. A module that only asks is issued
// no kinds (they are issued for a kinded bench a module uses or watches, and this module uses the operator
// channel alone), so an empty list says nothing — read as "none" it refused every proposal on the live mesh on
// 2026-10-10, with Telegram linked and proven.
func approverReady(bus AskBus) string {
kinds, err := bus.Kinds()
if err != nil || len(kinds) == 0 {
return ""
}
channels := 0
for _, k := range kinds {
if k.Seat == "channel" {
channels++
}
}
if channels == 0 {
return ""
}
for _, k := range kinds {
if k.Seat == "channel" && k.Promises("choice") && k.Promises("exact-render") && k.Promises("verified-sender") {
return ""
}
}
return "no channel that proves who answers (one promising a verified sender, such as Telegram) is on the mesh"
}
func newProposalID() string {
var b [8]byte
_, _ = rand.Read(b[:])
return "instr-" + hex.EncodeToString(b[:])
}
// Propose records a proposal and asks the operator. Nothing becomes active here or anywhere: every node applies it
// only on the operator's warrant, which it proves itself (Reconcile).
func Propose(p Paths, in ProposeInput, store ProposalStore, bus AskBus, now time.Time) (map[string]any, error) {
refuse := func(why string) (map[string]any, error) {
return nil, errors.New(why + ". Nothing was proposed")
}
if in.Scope != ScopeMesh && in.Scope != ScopeNode {
return refuse(fmt.Sprintf("%q is not a scope a shared section is proposed at: mesh (every node) or node (the nodes given); "+
"a home is registered with claude_code_instructions_register", in.Scope))
}
if !itemName.MatchString(in.Name) {
return refuse(fmt.Sprintf("%q is not a name: lower-case letters, digits and -, at most 64", in.Name))
}
var nodes []string
if in.Scope == ScopeNode {
for _, n := range in.Nodes {
if n == "all" {
return refuse("\"all\" is the mesh scope: propose it with scope mesh")
}
if !contains(nodes, n) {
nodes = append(nodes, n)
}
}
if len(nodes) == 0 {
nodes = []string{p.Node}
}
if problem := nodesProblem(nodes); problem != "" {
return refuse(problem)
}
sort.Strings(nodes)
} else if len(in.Nodes) > 0 {
return refuse("the mesh scope reaches every node; name nodes with scope node")
}
approved, err := ReadApproved(p)
if err != nil {
return nil, err
}
active, isActive := approved.Sections[sectionKey(in.Scope, nodes, in.Name)]
prop := Proposal{ID: newProposalID(), Scope: in.Scope, Nodes: nodes, Name: in.Name, From: p.Node, At: now.UTC()}
switch {
case in.Remove:
if !isActive {
return refuse(fmt.Sprintf("no approved section %q at the %s scope is held on this node to remove; one set at the "+
"controller's terminal is removed there", in.Name, in.Scope))
}
prop.Action, prop.Digest = ActionRemove, active.Digest
default:
if problem := ProposedTextProblem(in.Text); problem != "" {
return refuse(problem)
}
prop.Action, prop.Text, prop.Digest = ActionAdd, in.Text, textDigest(in.Text)
if isActive {
if active.Digest == prop.Digest {
return refuse(fmt.Sprintf("the section %q already holds exactly this text", in.Name))
}
prop.Action = ActionReplace
}
}
if terminal, _ := terminalText(p); len(terminal) > 0 {
for _, it := range terminal {
if it.Kind == KindInstructions && it.Name == in.Name {
return refuse(fmt.Sprintf("a section %q is set at the controller's terminal; it is changed there", in.Name))
}
}
}
if why := approverReady(bus); why != "" {
return refuse(why + "; " + AtTheTerminalInstead)
}
if waiting := waitingProposals(store, approved, now); waiting >= asks.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))
}
if problem := ProposalProblem(prop); problem != "" {
return refuse(problem)
}
ask := prop.Ask()
if err := ask.Check(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 {
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)
return nil, fmt.Errorf("the operator could not be asked (%v); the proposal is kept and will never become active. "+
"Propose it again, or %s", err, AtTheTerminalInstead)
}
say("proposal %s (%s %s %s, fingerprint %s) asked of the operator", prop.ID, prop.Action, prop.Scope, prop.Name, prop.Fingerprint())
// The router's word, before answering: it refuses at once an ask no channel can carry, and records one it took.
taken := "the router has not said yet whether it took the ask; claude_code_proposals shows it"
defer func() { endedMu.Lock(); delete(ended, prop.ID); endedMu.Unlock() }()
for deadline := time.Now().Add(RouterAnswersWithin); time.Now().Before(deadline); {
if w, ended := endedAsk(prop.ID); ended {
return nil, fmt.Errorf("the router did not ask the operator: the ask %s (%s). Nothing is active anywhere; %s",
w.Outcome, w.Words, AtTheTerminalInstead)
}
if entry, err := bus.Record(prop.ID); err == nil && entry != nil {
taken = "the router took the ask and shows it on the channels that can carry it"
break
}
time.Sleep(RouterAnswersEvery)
}
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),
"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
}
// waitingProposals counts the proposals still open: asked, not decided here, not expired.
func waitingProposals(store ProposalStore, approved Approved, now time.Time) int {
keys, err := store.Keys()
if err != nil {
return 0
}
n := 0
for _, k := range keys {
prop, ok := readProposal(store, k)
if !ok || prop.Asked != "" {
continue
}
if _, decided := approved.Decided[prop.ID]; decided || now.After(prop.At.Add(ProposalLasts)) {
continue
}
n++
}
return n
}
func readProposal(store ProposalStore, key string) (Proposal, bool) {
raw, found, err := store.Get(key)
if err != nil || !found {
return Proposal{}, false
}
var prop Proposal
if json.Unmarshal(raw, &prop) != nil || prop.ID != key {
return Proposal{}, false
}
return prop, true
}
// How long Propose waits for the router's word on a new ask, and how often it looks.
var (
RouterAnswersWithin = 8 * time.Second
RouterAnswersEvery = 250 * time.Millisecond
)
// ended is the asks the router ended without a choice, as heard on the decided subject — which only the router may
// publish, for this module alone. A refused ask leaves no record of the router's, so this is where its refusal is
// read.
var (
endedMu sync.Mutex
ended = map[string]asks.Warrant{}
)
func endedAsk(id string) (asks.Warrant, bool) {
endedMu.Lock()
defer endedMu.Unlock()
w, ok := ended[id]
return w, ok
}
// 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) {
return "", nil
}
reconcileMu.Lock()
defer reconcileMu.Unlock()
prop, known := readProposal(store, w.Ask)
if !known || (w.About != "" && w.About != prop.Ask().About) {
return "", nil
}
endedMu.Lock()
ended[w.Ask] = w
endedMu.Unlock()
approved, err := ReadApproved(p)
if err != nil {
return "", err
}
if _, done := approved.Decided[w.Ask]; done {
return "", nil
}
// 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}
if w.Words != "" {
d.Outcome += " (" + w.Words + ")"
}
approved.Decided[w.Ask] = d
delete(approved.Refused, w.Ask)
if err := writeApproved(p, approved); err != nil {
return "", err
}
return "proposal " + w.Ask + ": " + d.Outcome, nil
}
// ---- deciding, on every node ------------------------------------------------------------------------
// 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"`
}
var reconcileMu sync.Mutex
// Reconcile decides, on this node, every proposal it has not decided yet whose ask the router has closed, each held
// to the warrant as described at the top of this file, oldest warrant first, and never one older than the last
// change applied to the same section. It answers whether what this node renders changed, and a line for each
// decision.
func Reconcile(p Paths, store ProposalStore, bus AskBus, record Recorder, now time.Time) (bool, []string, error) {
reconcileMu.Lock()
defer reconcileMu.Unlock()
approved, err := ReadApproved(p)
if err != nil {
return false, nil, err // nothing is decided over a record that cannot be read
}
keys, err := store.Keys()
if err != nil {
return false, nil, err
}
type closed struct {
prop Proposal
w asks.Warrant
}
var ready []closed
discarded := false
for _, k := range keys {
prop, ok := readProposal(store, k)
if !ok {
continue
}
d, done := approved.Decided[prop.ID]
if done && !d.Heard {
continue
}
entry, err := bus.Record(prop.ID)
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 {
ready = append(ready, closed{prop, *rec.Warrant})
}
continue
}
if err == nil && entry == nil && now.After(prop.At.Add(ProposalLasts+time.Hour)) {
// Ended, and the router keeps no record of it: refused before it was taken, or kept past its time. Either
// way no answer can be proven, so it is discarded and nothing is applied.
approved.Decided[prop.ID] = ProposalDecision{At: now.UTC(),
Outcome: "discarded: the router keeps no record of its ask (refused before it was taken, or forgotten)"}
delete(approved.Refused, prop.ID)
discarded = true
continue
}
if err != nil || entry == nil {
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 {
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) })
changed, wrote := false, discarded
var said []string
for _, c := range ready {
d, applied, line, final := decide(p, &approved, c.prop, c.w, now)
if !final {
// Not held here now: said once per reason, and judged again on the next look.
if approved.Refused[c.prop.ID] != d.Outcome {
approved.Refused[c.prop.ID] = d.Outcome
said = append(said, line)
wrote = true
}
continue
}
delete(approved.Refused, c.prop.ID)
approved.Decided[c.prop.ID] = d
changed = changed || applied
said = append(said, line)
wrote = true
}
// A refusal is kept while the router keeps its record: after that it can never hold.
for id := range approved.Refused {
if prop, ok := readProposal(store, id); !ok || now.Sub(prop.At) > ProposalLasts+RouterKeeps {
delete(approved.Refused, id)
wrote = true
}
}
// The hand-act log: one entry per warrant mesh-wide (the controller keeps one per ask, read from the router's
// own record), asked by every node that decided it, and asked again every ten minutes for a week while the
// controller has not taken it.
if record != nil {
for id, d := range approved.Decided {
if d.Recorded || !d.Warranted || now.Sub(d.At) > RecordFor ||
(!d.LastTry.IsZero() && now.Sub(d.LastTry) < RecordEvery) {
continue
}
d.LastTry = now.UTC()
if err := record(id); err != nil {
d.Record = "not recorded in the hand-act log yet: " + err.Error()
} else {
d.Recorded, d.Record = true, ""
}
approved.Decided[id] = d
wrote = true
}
}
if !wrote {
return false, nil, nil
}
if err := writeApproved(p, approved); err != nil {
return false, said, fmt.Errorf("what this node decided could not be kept: %w", err)
}
return changed, said, nil
}
// 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()}
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
}
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 {
d.Outcome = "discarded: the ask " + string(w.Outcome)
if w.Words != "" {
d.Outcome += " (" + w.Words + ")"
}
return d, false, "proposal " + prop.ID + ": " + d.Outcome, true
}
// The record is judged as this module would have made it: anything holding a tool runner's credential can
// write one, and ask for it under this module's name.
if problem := ProposalProblem(prop); problem != "" {
return refused("the proposal is not one this module makes: " + problem)
}
option, err := w.For(Module, prop.Ask())
if err != nil {
return refused(err.Error())
}
if err := option.Performs(prop.Act(option.ID)); err != nil {
return refused(err.Error())
}
d.Warranted = true
if option.ID == OptionDecline {
d.Outcome = "declined"
return d, false, "proposal " + prop.ID + ": declined by the operator; nothing changes", true
}
if option.ID != OptionApprove {
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) {
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
}
d.Outcome = "approved"
reaches := prop.Scope == ScopeMesh || contains(prop.Nodes, p.Node)
switch prop.Action {
case ActionRemove:
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()
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()}
}
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())
return d, reaches, "proposal " + prop.ID + ": approved, " + prop.Action + " " + prop.Name, true
}
// ---- the list ---------------------------------------------------------------------------------------
// ListProposals is every proposal on the bus, newest first, with where the router has it and what this node made
// of it; with an id, that one with its whole text. And the sections approved here.
func ListProposals(p Paths, store ProposalStore, bus AskBus, id string, now time.Time) (map[string]any, error) {
approved, err := ReadApproved(p)
if err != nil {
return nil, err
}
keys, err := store.Keys()
if err != nil {
return nil, err
}
var props []Proposal
for _, k := range keys {
if prop, ok := readProposal(store, k); ok && (id == "" || prop.ID == id) {
props = append(props, prop)
}
}
if id != "" && len(props) == 0 {
return nil, fmt.Errorf("no proposal %q is kept on the bus", id)
}
sort.Slice(props, func(i, j int) bool { return props[i].At.After(props[j].At) })
var rows []map[string]any
for _, prop := range props {
row := map[string]any{"id": prop.ID, "action": prop.Action, "name": prop.Name, "reaches": prop.where(),
"from": prop.From, "at": prop.At.Format(time.RFC3339), "fingerprint": prop.Fingerprint(), "lines": prop.Lines(),
"bytes": len(prop.Text)}
if d, ok := approved.Decided[prop.ID]; ok {
row["here"] = d
} else if why, ok := approved.Refused[prop.ID]; ok {
row["here"] = "the answer did not hold here, judged again on every look: " + why
} else if prop.Asked != "" {
row["here"] = prop.Asked + "; it never becomes active"
} else if now.After(prop.At.Add(ProposalLasts + RouterKeeps)) {
row["here"] = "cannot be proven here: the router keeps its record of an answer 30 days, and this node never " +
"read it, so nothing is rendered here; propose it again, or set it at the controller's terminal"
} else if now.After(prop.At.Add(ProposalLasts)) {
row["here"] = "not decided here yet: the ask has ended, and the router's word on it has not been read here"
} else {
row["here"] = "waiting for the operator's answer; nothing is active"
}
if id != "" {
row["text"] = prop.Text
row["digest"] = prop.Digest
if problem := ProposalProblem(prop); problem != "" {
row["warning"] = "no node will apply this proposal: " + problem
}
// Whether the operator's phone shows this text: the ask composed here from the record against the ask
// 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 {
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():
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"
}
}
}
}
rows = append(rows, row)
}
var here []map[string]any
for _, s := range ApprovedHere(approved, p.Node) {
here = append(here, map[string]any{"name": s.Name, "scope": s.Scope, "nodes": s.Nodes, "proposal": s.Proposal,
"from": s.From, "approved": s.Approved.Format(time.RFC3339), "by": s.By,
"fingerprint": fingerprintOf(s.Digest)})
}
return map[string]any{"proposals": rows, "approved here": here,
"how": "a proposal becomes active on a node only when that node has held the operator's warrant, read from the " +
"router's own record, to the proposal's exact text (novox/hq ADR 0274)"}, nil
}