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:
@@ -122,7 +122,7 @@ The agent kept reaching for `ssh <machine> journalctl` while the service manager
|
||||
- **The "instead of" table.** Every seat verb and module tool may say which shell commands it replaces
|
||||
(`replaces`, in the seat's definition or the module's manifest). The module reads the bus — the seats' verbs from the
|
||||
seats' contract, the placed modules' manifests from `mesh-delivery-indexer.modules` and `.manifests`, the
|
||||
machines and where each module runs from `mesh-delivery-assigner.placement` (issue 1126: the controller that
|
||||
machines and where each module runs from `mesh-delivery-assigner.assignments` (issue 1126: the controller that
|
||||
answered this is not placed) — at start, every ten minutes and on `claude_code_render`, keeps it in its
|
||||
state (`mesh-tools.json`), and renders it into `CLAUDE.md` as one row per seat or module: `<node>/<seat>.` and
|
||||
each verb with the commands it replaces. Generated, never written by hand: a verb that gains `replaces` is in
|
||||
@@ -206,10 +206,20 @@ judges the record as this module would have made it, composes the ask again from
|
||||
the text's digest, in time), and only then writes the section into its own `approved.json`, which the managed
|
||||
instruction file renders under "Instructions the operator approved". A proposal is decided once per node, and an approval older than the last change applied to its section is never applied; a record
|
||||
deleted from the bus removes nothing. Refused: shell lines, front matter, any `@` after a non-letter (a file the instruction file may read in), invisible or control characters, headings of level one to three, anything shaped like
|
||||
a secret, more than 16 KiB, a name the terminal sets, and — at once, naming the terminal's line — whenever no channel
|
||||
a secret, more than 16 KiB, and — at once, naming the terminal's line — whenever no channel
|
||||
that proves its sender is there. The controller's hand-act log records each warrant once (`warranted`), read from the
|
||||
router's record. The text is never logged.
|
||||
|
||||
**A section registered before is replaced or removed the same way** (hq issue 340, note 14): a section registered for the
|
||||
mesh or the node before the controller's terminal took that over, or set there, is proposed over by name. The proposal
|
||||
carries `over`, the digest of the registered text, which the act the operator's answer binds names; the ask says
|
||||
plainly that it replaces (or removes) the registered section, with the registered text's fingerprint, and shows the new
|
||||
text as any other. On Approve each node writes the section into `approved.json` with that `over`, and the managed
|
||||
instruction file renders it in the place of the registered section (a removal leaves the section out); on Decline or at
|
||||
expiry nothing changes. A replacement keeps the registered section's own heading form, because it replaces a mesh
|
||||
section; the heading rule stays for every new shared section. `claude_code_proposals` shows each proposal's `registered
|
||||
section` and `claude_code_registrations` lists what approvals replaced or removed.
|
||||
|
||||
**A session asks the operator a question on Telegram** (hq ADR 0332, issue 589). An agent that needs a decision while
|
||||
the operator may be away from the terminal calls `claude_code_ask` with a headline, the context in plain words, two to
|
||||
four options (a label and what choosing it means, exactly one recommended, with why), what it does if nobody
|
||||
|
||||
@@ -637,6 +637,14 @@ func mergeSettings(a, b map[string]any) map[string]any {
|
||||
// registered for the mesh, then those for this node.
|
||||
func InstructionSections(c Config) string {
|
||||
var b strings.Builder
|
||||
// The approved sections that replace or remove a registered one, by its name and the digest of its text, and the
|
||||
// proposals already placed where the registered section stood.
|
||||
over, placed := map[string]ApprovedSection{}, map[string]bool{}
|
||||
for _, s := range c.Approved {
|
||||
if s.Over != "" {
|
||||
over[s.Name+"|"+s.Over] = s
|
||||
}
|
||||
}
|
||||
registered := "Registered through the `claude-code` module before that moved to the controller's terminal; change them there, in the module's `instructions` setting."
|
||||
for _, part := range []struct {
|
||||
title, note string
|
||||
@@ -654,14 +662,31 @@ func InstructionSections(c Config) string {
|
||||
}
|
||||
fmt.Fprintf(&b, "\n## %s\n\n%s\n", part.title, part.note)
|
||||
for _, it := range sections {
|
||||
fmt.Fprintf(&b, "\n### %s\n\n%s\n", it.Name, strings.TrimSpace(it.Files[it.Name+".md"]))
|
||||
text := it.Files[it.Name+".md"]
|
||||
if s, ok := over[it.Name+"|"+textDigest(text)]; ok {
|
||||
// What the operator approved over this registered section stands in its place, or leaves it out.
|
||||
placed[s.Proposal] = true
|
||||
if s.Removes {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "\n### %s\n\n*Replaced on the operator's approval (proposed from %s; approved %s).*\n\n%s\n", it.Name, s.From,
|
||||
s.Approved.UTC().Format("2006-01-02 15:04 UTC"), strings.TrimSpace(s.Text))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "\n### %s\n\n%s\n", it.Name, strings.TrimSpace(text))
|
||||
}
|
||||
}
|
||||
if len(c.Approved) > 0 {
|
||||
var approved []ApprovedSection
|
||||
for _, s := range c.Approved {
|
||||
if !s.Removes && !placed[s.Proposal] {
|
||||
approved = append(approved, s)
|
||||
}
|
||||
}
|
||||
if len(approved) > 0 {
|
||||
b.WriteString("\n## Instructions the operator approved\n\nProposed by an agent through the `claude-code` module and " +
|
||||
"approved by the operator on a channel that proves who answered (novox/hq ADR 0274). Changed or removed only the " +
|
||||
"same way: `claude_code_instructions_propose`.\n")
|
||||
for _, s := range c.Approved {
|
||||
for _, s := range approved {
|
||||
fmt.Fprintf(&b, "\n### %s\n\n*Proposed from %s; approved %s.*\n\n%s\n", s.Name, s.From,
|
||||
s.Approved.UTC().Format("2006-01-02 15:04 UTC"), strings.TrimSpace(s.Text))
|
||||
}
|
||||
|
||||
@@ -116,14 +116,19 @@ func proposalTools(p Paths) []stdio.Tool {
|
||||
"(Telegram), shown the text (whole when it is short, else its first lines and its fingerprint), and the text " +
|
||||
"reaches those sessions only on Approve. Decline, or no answer within a day, discards it. Refused for text " +
|
||||
"that would run or read in something by itself (a shell line, front matter, an @ file reference), anything " +
|
||||
"shaped like a secret, more than 16 KiB, a name set at the controller's terminal, or when no such channel is " +
|
||||
"there (novox/hq ADR 0274, issue 340). claude_code_proposals shows where it stands.",
|
||||
"shaped like a secret, more than 16 KiB, or when no such channel is there (novox/hq ADR 0274, issue 340). A name " +
|
||||
"registered before for the mesh or the node, or set at the controller's terminal, is proposed too: as a " +
|
||||
"replacement (the text) or with remove a removal of that registered section. The operator's ask says plainly " +
|
||||
"that it replaces or removes the registered section, and shows the text the same way; on Approve every node " +
|
||||
"renders the new text in the place of the old, on Decline or at expiry nothing changes. A replacement of a " +
|
||||
"registered section may keep that section's own heading form (it replaces a mesh section); the heading rule " +
|
||||
"stays for every new shared section. claude_code_proposals and claude_code_registrations show where it stands.",
|
||||
Input: map[string]any{
|
||||
"scope": str("mesh (every node) or node (the nodes given, or this one)"),
|
||||
"nodes": str("for scope node: a comma-separated list of nodes; absent is this node"),
|
||||
"name": str("the section's name: lower-case letters, digits and -, e.g. handoff-from-g14"),
|
||||
"text": str("the section's text, markdown, as every session will read it"),
|
||||
"remove": map[string]any{"type": "boolean", "description": "propose removing the approved section of this name and scope instead"},
|
||||
"remove": map[string]any{"type": "boolean", "description": "propose removing the approved or registered section of this name and scope instead"},
|
||||
},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
if proposing == nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -399,11 +399,10 @@ func TestWhatASessionWouldRunIsRefused(t *testing.T) {
|
||||
if _, err := Propose(p, ProposeInput{Scope: ScopeNode, Nodes: []string{"all"}, Name: "x", Text: "t\n"}, store, r, t0); err == nil {
|
||||
t.Error("all at the node scope was taken")
|
||||
}
|
||||
// A name the terminal sets is changed there.
|
||||
// A name the terminal sets is replaced by a proposal over it (TestARegisteredSectionIsReplacedOrRemovedOnTheOperatorsApproval).
|
||||
writeFile(t, p.Settings, `{"instructions": {"x": ["set at the terminal"]}}`)
|
||||
if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "x", Text: "t\n"}, store, r, t0); err == nil ||
|
||||
!strings.Contains(err.Error(), "controller's terminal") {
|
||||
t.Errorf("a terminal name was proposed: %v", err)
|
||||
if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "x", Text: "t\n"}, store, r, t0); err != nil {
|
||||
t.Errorf("a terminal name was not proposed over: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,24 @@ func Registrations(p Paths, servers ServerState, config ConfigState) (map[string
|
||||
for _, it := range text {
|
||||
shared = append(shared, it.Kind+" "+it.Name)
|
||||
}
|
||||
// The registered text an approved proposal replaced or removed on this node: what renders instead, or nothing.
|
||||
replaced := []map[string]any{}
|
||||
if approved, err := ReadApproved(p); err == nil {
|
||||
for _, s := range ApprovedHere(approved, p.Node) {
|
||||
if s.Over == "" {
|
||||
continue
|
||||
}
|
||||
did := "replaced"
|
||||
if s.Removes {
|
||||
did = "removed"
|
||||
}
|
||||
replaced = append(replaced, map[string]any{"name": s.Name, "scope": s.Scope, "did": did,
|
||||
"registered text": fingerprintOf(s.Over), "proposal": s.Proposal, "approved": s.Approved.Format("2006-01-02T15:04:05Z07:00"),
|
||||
"by": s.By})
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"registered text replaced by an approved proposal, or removed by one": replaced,
|
||||
"set at the controller's terminal, as this node composes it": map[string]any{
|
||||
"managed_settings": settings.ManagedSettings, "mcp_servers": settings.MCPServers,
|
||||
"shared text": shared, "shared text left out": leftOut},
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A section registered for the mesh before the controller's terminal took that over, or set at the terminal, is
|
||||
// replaced or removed by a proposal the operator approves (novox/hq ADR 0274, issue 340 note 14).
|
||||
|
||||
const registeredOld = "# What this mesh is\n\n## Its purpose\n\nThe old words.\n"
|
||||
const registeredNew = "# What this mesh is\n\n## Its purpose\n\nThe new words.\n"
|
||||
|
||||
// register places the old text the way the node keeps it: in the registered items (mesh scope) or in the settings.
|
||||
func register(t *testing.T, p Paths, where string) {
|
||||
t.Helper()
|
||||
switch where {
|
||||
case "registered":
|
||||
raw, _ := json.Marshal(map[string]Item{"mesh.instructions.what-this-mesh-is": {Kind: KindInstructions,
|
||||
Name: "what-this-mesh-is", Scope: ScopeMesh, Files: one("what-this-mesh-is", registeredOld), By: "g14"}})
|
||||
writeFile(t, p.config(), string(raw))
|
||||
case "terminal":
|
||||
raw, _ := json.Marshal(map[string]any{"instructions": map[string]string{"what-this-mesh-is": registeredOld}})
|
||||
writeFile(t, p.Settings, string(raw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestARegisteredSectionIsReplacedOrRemovedOnTheOperatorsApproval(t *testing.T) {
|
||||
for _, where := range []string{"registered", "terminal"} {
|
||||
t.Run(where, func(t *testing.T) {
|
||||
p, _ := node(t, "g14")
|
||||
other, _ := node(t, "ace")
|
||||
register(t, p, where)
|
||||
register(t, other, where)
|
||||
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
|
||||
propose := func(in ProposeInput, at time.Time) string {
|
||||
t.Helper()
|
||||
in.Scope, in.Name = ScopeMesh, "what-this-mesh-is"
|
||||
out, err := Propose(p, in, store, r, at)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out["proposed"].(string)
|
||||
}
|
||||
// The same text is no change; a new section with such headings is still refused.
|
||||
if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "what-this-mesh-is", Text: registeredOld}, store, r, t0); err == nil {
|
||||
t.Error("the registered text was proposed over itself")
|
||||
}
|
||||
if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "fresh", Text: registeredNew}, store, r, t0); err == nil {
|
||||
t.Error("a new section may hold the mesh's own headings")
|
||||
}
|
||||
|
||||
id := propose(ProposeInput{Text: registeredNew}, t0)
|
||||
a := r.published[id]
|
||||
if a.Headline != "Replace Claude instructions 'what-this-mesh-is'?" ||
|
||||
!strings.Contains(a.Explanation, "in place of the instructions 'what-this-mesh-is' registered before") {
|
||||
t.Errorf("the ask does not say it replaces the registered section: %q / %q", a.Headline, a.Explanation)
|
||||
}
|
||||
if !strings.Contains(a.Details, "It replaces the registered instructions of this name, whose text has the fingerprint "+
|
||||
fingerprintOf(textDigest(registeredOld))) || !strings.Contains(a.Details, "The new words.") {
|
||||
t.Errorf("the ask does not show the text and what it replaces: %q", a.Details)
|
||||
}
|
||||
if got := renderedInstructions(t, p); !strings.Contains(got, "The old words.") || strings.Contains(got, "The new words.") {
|
||||
t.Fatal("a proposal changed what renders")
|
||||
}
|
||||
// Declined, nothing changes.
|
||||
r.close(r.chose(id, OptionDecline, t0.Add(time.Hour)))
|
||||
if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(time.Hour)); changed {
|
||||
t.Error("a decline changed what renders")
|
||||
}
|
||||
// Approved, the registered section is replaced in place, on every node that proves the warrant.
|
||||
id = propose(ProposeInput{Text: registeredNew}, t0.Add(2*time.Hour))
|
||||
r.close(r.chose(id, OptionApprove, t0.Add(3*time.Hour)))
|
||||
for _, n := range []Paths{p, other} {
|
||||
if changed, _, err := Reconcile(n, store, r, nil, t0.Add(3*time.Hour)); err != nil || !changed {
|
||||
t.Fatalf("%s: the approval changed nothing (%v)", n.Node, err)
|
||||
}
|
||||
got := renderedInstructions(t, n)
|
||||
if strings.Contains(got, "The old words.") || strings.Count(got, "The new words.") != 1 ||
|
||||
strings.Count(got, "### what-this-mesh-is") != 1 {
|
||||
t.Fatalf("%s: the section was not replaced once, in place:\n%s", n.Node, got)
|
||||
}
|
||||
if strings.Contains(got, "## Instructions the operator approved") {
|
||||
t.Errorf("%s: a replacement is listed apart from the section it replaces", n.Node)
|
||||
}
|
||||
}
|
||||
// claude_code_registrations and claude_code_proposals show it.
|
||||
reg, err := Registrations(p, &bus{kept: map[string]map[string]any{}}, memConfig{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(reg)
|
||||
if !strings.Contains(string(raw), "registered text replaced by an approved proposal") {
|
||||
t.Errorf("claude_code_registrations does not show the replacement: %s", raw)
|
||||
}
|
||||
list, _ := ListProposals(p, store, r, "", t0.Add(4*time.Hour))
|
||||
raw, _ = json.Marshal(list)
|
||||
if !strings.Contains(string(raw), "replaces the registered section whose text") {
|
||||
t.Errorf("claude_code_proposals does not show the replacement: %s", raw)
|
||||
}
|
||||
|
||||
// A removal of the registered section is asked the same way and leaves nothing.
|
||||
rid := propose(ProposeInput{Remove: true}, t0.Add(5*time.Hour))
|
||||
if got := r.published[rid]; got.Headline != "Remove Claude instructions 'what-this-mesh-is'?" {
|
||||
t.Errorf("removal ask: %q", got.Headline)
|
||||
}
|
||||
r.close(r.chose(rid, OptionApprove, t0.Add(6*time.Hour)))
|
||||
if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(6*time.Hour)); !changed {
|
||||
t.Fatal("the approved removal changed nothing")
|
||||
}
|
||||
if got := renderedInstructions(t, p); strings.Contains(got, "what-this-mesh-is") {
|
||||
t.Errorf("the removal left the section:\n%s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestARegisteredSectionIsRemovedByAProposalToo(t *testing.T) {
|
||||
p, _ := node(t, "g14")
|
||||
register(t, p, "registered")
|
||||
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
|
||||
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "what-this-mesh-is", Remove: true}, store, r, t0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := out["proposed"].(string)
|
||||
if !strings.Contains(r.published[id].Explanation, "registered") || !strings.Contains(r.published[id].Details, "registered text it removes") {
|
||||
t.Errorf("the ask does not say it removes a registered section: %+v", r.published[id])
|
||||
}
|
||||
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
|
||||
if _, _, err := Reconcile(p, store, r, nil, t0.Add(time.Hour)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := renderedInstructions(t, p); strings.Contains(got, "The old words") {
|
||||
t.Errorf("the registered section still renders:\n%s", got)
|
||||
}
|
||||
// Nothing registered and nothing approved: still refused.
|
||||
if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "nothing-here", Remove: true}, store, r, t0); err == nil {
|
||||
t.Error("a section nobody holds was removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnApprovalIsBoundToTheRegisteredTextItReplaces(t *testing.T) {
|
||||
prop := Proposal{ID: "instr-0123456789abcdef", Action: ActionReplace, Scope: ScopeMesh, Name: "x", Text: "t\n", Digest: textDigest("t\n"),
|
||||
From: "g14", At: t0}
|
||||
plain := prop.Act(OptionApprove)
|
||||
if _, ok := plain["over"]; ok {
|
||||
t.Error("a proposal over nothing binds an over: its older warrants would no longer hold")
|
||||
}
|
||||
prop.Over = textDigest("old\n")
|
||||
if prop.Act(OptionApprove)["over"] != prop.Over {
|
||||
t.Error("the act does not bind the registered text it replaces")
|
||||
}
|
||||
prop.Over = "not a digest"
|
||||
if ProposalProblem(prop) == "" {
|
||||
t.Error("a proposal over a text with no digest is one this module makes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestANodeSectionRegisteredForThisNodeIsReplacedOnlyThere(t *testing.T) {
|
||||
p, _ := node(t, "g14")
|
||||
raw, _ := json.Marshal(map[string]Item{"node.g14.instructions.local": {Kind: KindInstructions, Name: "local", Scope: ScopeNode,
|
||||
Files: one("local", "## Local\n\nold\n"), By: "g14"}})
|
||||
writeFile(t, p.config(), string(raw))
|
||||
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
|
||||
out, err := Propose(p, ProposeInput{Scope: ScopeNode, Nodes: []string{"g14"}, Name: "local", Text: "## Local\n\nnew\n"}, store, r, t0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := out["proposed"].(string)
|
||||
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
|
||||
if _, _, err := Reconcile(p, store, r, nil, t0.Add(time.Hour)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := renderedInstructions(t, p)
|
||||
if strings.Contains(got, "\nold\n") || !strings.Contains(got, "\nnew\n") || strings.Count(got, "### local") != 1 {
|
||||
t.Errorf("the node's section was not replaced in place:\n%s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user