A section registered before the controller's terminal took over, or set there, is replaced or removed by a proposal the operator approves

It could not be changed at all: propose refused a name set there and never replaced one. The ask now says plainly that
it replaces or removes the registered section and binds its text's digest; on Approve every node renders the new text
in the place of the old (a removal leaves it out); a replacement keeps the section's own heading form (issue 340 note 14).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:14:55 +01:00
parent 2dc9e07e88
commit 657cb2b6c8
7 changed files with 387 additions and 36 deletions
+138 -24
View File
@@ -113,6 +113,10 @@ type Proposal struct {
Form int `json:"form,omitempty"`
// Asked is empty once the ask was published, or says why it was not; nothing composes from it.
Asked string `json:"asked,omitempty"`
// Over is the digest of the text of a section registered before (for the mesh or the node, or set at the
// controller's terminal) that this proposal replaces or removes (novox/hq issue 340): the ask says so, and an
// approval is bound to it. Empty for a proposal that touches no registered section.
Over string `json:"over,omitempty"`
}
// textDigest is the digest a proposal binds: SHA-256 over the text's bytes.
@@ -166,9 +170,14 @@ 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) occ.Act {
return occ.Act{"verb": Module + ".instructions", "option": option, "proposal": p.ID, "action": p.Action,
act := 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)}
if p.Over != "" {
// Only when set, so the act of a proposal made before keeps the digest its warrant names.
act["over"] = p.Over
}
return act
}
// AskGlance is the form of an ask read at a glance (Proposal.Form, Question.Form; novox/hq issue 639).
@@ -199,8 +208,12 @@ func (p Proposal) glanceAsk() oc.Ask {
}
var d strings.Builder
if p.Action == ActionRemove {
fmt.Fprintf(&d, "Proposed through the claude-code module on %s at %s UTC. The approved text it removes has the fingerprint %s.",
p.From, p.At.UTC().Format("15:04 on 2 Jan"), p.Fingerprint())
removes := "The approved text it removes"
if p.Over != "" {
removes = "The registered text it removes"
}
fmt.Fprintf(&d, "Proposed through the claude-code module on %s at %s UTC. %s has the fingerprint %s.",
p.From, p.At.UTC().Format("15:04 on 2 Jan"), removes, p.Fingerprint())
} else {
if whole {
d.WriteString("The text:\n" + shown + "\n")
@@ -209,6 +222,9 @@ func (p Proposal) glanceAsk() oc.Ask {
}
fmt.Fprintf(&d, "Proposed through the claude-code module on %s at %s UTC: %d lines, %d bytes, fingerprint %s.",
p.From, p.At.UTC().Format("15:04 on 2 Jan"), p.Lines(), len(p.Text), p.Fingerprint())
if p.Over != "" {
fmt.Fprintf(&d, " It replaces the registered instructions of this name, whose text has the fingerprint %s.", fingerprintOf(p.Over))
}
}
fmt.Fprintf(&d, "\nRead all of it at the controller's terminal: mesh-cli ask claude-code claude_code_proposals '{\"id\": \"%s\"}'.", p.ID)
does := "the text becomes part of those sessions' instructions"
@@ -220,6 +236,16 @@ func (p Proposal) glanceAsk() oc.Ask {
// summary is what the proposal does, at a glance: its proposer's words, or the module's.
func (p Proposal) summary() string {
// A proposal over a registered section says so in the module's words, never the proposer's: the operator is
// told plainly which section of the mesh's own it replaces or removes.
if p.Over != "" {
where := strings.ToUpper(p.where()[:1]) + p.where()[1:]
if p.Action == ActionRemove {
return fmt.Sprintf("%s stops reading the instructions '%s' registered before, from its next start.", where, p.Name)
}
return fmt.Sprintf("%s reads these %d lines in place of the instructions '%s' registered before, from its next start.",
where, p.Lines(), p.Name)
}
if s := strings.TrimSpace(p.Summary); s != "" {
return s
}
@@ -461,7 +487,7 @@ func ProposalProblem(p Proposal) string {
}
switch p.Action {
case ActionAdd, ActionReplace:
if problem := ProposedTextProblem(p.Text); problem != "" {
if problem := proposedTextProblem(p.Text, p.Over != ""); problem != "" {
return problem
}
if textDigest(p.Text) != p.Digest {
@@ -474,6 +500,9 @@ func ProposalProblem(p Proposal) string {
default:
return "its action is not add, replace or remove"
}
if p.Over != "" && !strings.HasPrefix(p.Over, "sha256:") {
return "the digest of the registered text it replaces is not one"
}
return ""
}
@@ -489,7 +518,12 @@ var secretShaped = []*regexp.Regexp{
// 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 {
func ProposedTextProblem(text string) string { return proposedTextProblem(text, false) }
// proposedTextProblem is ProposedTextProblem; ownHeadings says the text replaces a section registered before, which
// may keep that section's own heading form (a replacement of a mesh section is a mesh section): the heading rule
// stays for every new shared section.
func proposedTextProblem(text string, ownHeadings bool) string {
switch {
case strings.TrimSpace(text) == "":
return "the text is empty"
@@ -515,6 +549,9 @@ func ProposedTextProblem(text string) string {
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 ownHeadings {
return secretProblem(text)
}
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"
}
@@ -525,6 +562,10 @@ func ProposedTextProblem(text string) string {
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))
}
return secretProblem(text)
}
func secretProblem(text string) string {
for _, re := range secretShaped {
if re.MatchString(text) {
return "the text holds something shaped like a secret, which a shared section never carries"
@@ -546,6 +587,11 @@ type ApprovedSection struct {
From string `json:"from"`
Approved time.Time `json:"approved"`
By string `json:"by"`
// Over is the digest of the registered section's text this one replaced or removed; the registered section of
// that name and text is left out of what renders. Removes marks a removal of a registered section: it holds no
// text, and its Digest is the fingerprint of the text removed.
Over string `json:"over,omitempty"`
Removes bool `json:"removes,omitempty"`
}
// ProposalDecision is what this node made of one proposal: once decided, never again.
@@ -652,8 +698,11 @@ 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) ||
textOK := proposedTextProblem(s.Text, s.Over != "") == "" && textDigest(s.Text) == s.Digest
if s.Removes {
textOK = s.Text == "" && s.Over != "" && strings.HasPrefix(s.Digest, "sha256:")
}
if !itemName.MatchString(s.Name) || !nodeName.MatchString(s.From) || !textOK || (s.Scope != ScopeMesh && s.Scope != ScopeNode) ||
(s.Scope == ScopeNode && (len(s.Nodes) == 0 || nodesProblem(s.Nodes) != "")) {
continue
}
@@ -810,30 +859,42 @@ func Propose(p Paths, in ProposeInput, store ProposalStore, bus AskBus, now time
}
prop := Proposal{ID: newProposalID(), Scope: in.Scope, Nodes: nodes, Name: in.Name, From: p.Node, At: now.UTC(),
Summary: strings.TrimSpace(in.Summary), Form: AskGlance}
registered, err := registeredSection(p, in.Scope, nodes, in.Name)
if err != nil {
return refuse(err.Error())
}
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))
switch {
case isActive && active.Removes:
return refuse(fmt.Sprintf("the section %q was removed already", in.Name))
case isActive:
// An approved section that replaced a registered one is removed with it: nothing of the name is left.
prop.Action, prop.Digest, prop.Over = ActionRemove, active.Digest, active.Over
case registered != "":
prop.Action, prop.Digest, prop.Over = ActionRemove, registered, registered
default:
return refuse(fmt.Sprintf("no approved or registered section %q at the %s scope is held on this node to remove", in.Name, in.Scope))
}
prop.Action, prop.Digest = ActionRemove, active.Digest
default:
if problem := ProposedTextProblem(in.Text); problem != "" {
prop.Over = registered
if prop.Over == "" && isActive {
prop.Over = active.Over
}
if problem := proposedTextProblem(in.Text, prop.Over != ""); problem != "" {
return refuse(problem)
}
prop.Action, prop.Text, prop.Digest = ActionAdd, in.Text, textDigest(in.Text)
if isActive {
if isActive && !active.Removes {
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))
} else if registered != "" {
if registered == prop.Digest {
return refuse(fmt.Sprintf("the registered section %q already holds exactly this text", in.Name))
}
prop.Action = ActionReplace
}
}
if why := approverReady(bus); why != "" {
@@ -1168,15 +1229,22 @@ func decide(p Paths, approved *Approved, prop Proposal, w oc.Warrant, now time.T
switch prop.Action {
case ActionRemove:
held, ok := approved.Sections[key]
if !ok || held.Digest != prop.Digest {
switch {
case prop.Over != "" && (!ok || (!held.Removes && held.Digest == prop.Digest)):
// A registered section is removed by leaving it out of what renders: the record of the removal says
// which text, and a node holding no such section renders nothing of it either way.
approved.Sections[key] = ApprovedSection{Name: prop.Name, Scope: prop.Scope, Nodes: prop.Nodes, Digest: prop.Digest,
Proposal: prop.ID, From: prop.From, Approved: at.UTC(), By: occ.Says(w), Over: prop.Over, Removes: true}
case !ok || held.Removes || held.Digest != prop.Digest:
d.Outcome = "approved, nothing to remove here: this node holds no section " + prop.Name + " with that fingerprint"
approved.Changed[key] = at.UTC()
return d, false, "proposal " + prop.ID + ": " + d.Outcome, true
default:
delete(approved.Sections, key)
}
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: at.UTC(), By: occ.Says(w)}
Digest: prop.Digest, Proposal: prop.ID, From: prop.From, Approved: at.UTC(), By: occ.Says(w), Over: prop.Over}
}
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))
@@ -1211,6 +1279,13 @@ func ListProposals(p Paths, store ProposalStore, bus AskBus, id string, now time
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 prop.Over != "" {
verb := "replaces"
if prop.Action == ActionRemove {
verb = "removes"
}
row["registered section"] = verb + " the registered section whose text has the fingerprint " + fingerprintOf(prop.Over)
}
if d, ok := approved.Decided[prop.ID]; ok {
row["here"] = d
} else if why, ok := approved.Refused[prop.ID]; ok {
@@ -1252,11 +1327,50 @@ func ListProposals(p Paths, store ProposalStore, bus AskBus, id string, now time
}
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,
row := 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)})
"fingerprint": fingerprintOf(s.Digest)}
if s.Over != "" {
row["registered section"] = "replaces the registered section whose text has the fingerprint " + fingerprintOf(s.Over)
if s.Removes {
row["registered section"] = "removes the registered section whose text has the fingerprint " + fingerprintOf(s.Over)
}
}
here = append(here, row)
}
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
}
// registeredSection is the digest of the text of the instruction section of this name that was registered before
// (for the mesh, or for this node when the proposal names it) or is set at the controller's terminal, or "" when
// there is none; an error when the places hold different texts under the name, since an approval is bound to one.
func registeredSection(p Paths, scope string, nodes []string, name string) (string, error) {
var items map[string]Item
_ = readJSON(p.config(), &items)
c := ConfigOf(Taken(items))
c.Terminal, _ = terminalText(p)
layers := [][]Item{c.Mesh, c.Terminal}
if scope == ScopeNode {
layers = [][]Item{c.Node, c.Terminal}
if !contains(nodes, p.Node) {
layers = [][]Item{c.Terminal}
}
}
digest := ""
for _, layer := range layers {
for _, it := range layer {
if it.Kind != KindInstructions || it.Name != name {
continue
}
d := textDigest(it.Files[name+".md"])
if digest != "" && d != digest {
return "", fmt.Errorf("the section %q is registered with different texts (for the mesh and at the controller's terminal); "+
"remove one of them first", name)
}
digest = d
}
}
return digest, nil
}