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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user