Run the router as its own account, ask unlinking of the linked account, and take no approval where an agent can become root (hq ADR 0259 §7, §8)

This commit is contained in:
jochen
2026-10-09 17:08:17 +02:00
committed by jschoubben
parent c2c75ceb3b
commit debb897c21
20 changed files with 937 additions and 111 deletions
+89 -5
View File
@@ -62,6 +62,11 @@ type AskRecord struct {
Told bool `json:"told,omitempty"`
// Record is the message the ask is shown on: a condition's key, or its own.
Record string `json:"record,omitempty"`
// Only is the one account that may answer it, on its kind: an ask of the router's own, put to the account
// an unlink or a relink concerns (novox/hq ADR 0259 §7).
Only *Identity `json:"only,omitempty"`
// Act is what the router does on its own ask's warrant: unlink, or link (selfact.go).
Act *SelfAct `json:"act,omitempty"`
}
// Identity is one account linked as the operator on a channel kind.
@@ -86,6 +91,8 @@ type AskStore interface {
GetLink(kind string) (*PendingLink, error)
PutLink(kind string, l PendingLink) error
DeleteLink(kind string) error
GetUnlinked(kind string) (*Unlinked, error)
PutUnlinked(kind string, u Unlinked) error
}
type askEntry struct {
@@ -161,14 +168,15 @@ func (h *Holder) carriers(level asks.Level) []string {
}
var out []string
for _, k := range h.Bench.Kinds() {
if h.Bench.Ready(k.Kind) != nil || !h.Bench.Promises(k.Kind, "choice") {
// Every level needs a choice made in one act, shown exactly as the router rendered it (ADR 0234 §8).
if h.Bench.Ready(k.Kind) != nil || !h.Bench.Promises(k.Kind, "choice") || !h.Bench.Promises(k.Kind, "exact-render") {
continue
}
switch level {
case asks.Acknowledge:
out = append(out, k.Kind)
case asks.Approve:
if h.Bench.Promises(k.Kind, "verified-sender") && h.linked(k.Kind) {
if h.Bench.Promises(k.Kind, "verified-sender") && h.linked(k.Kind) && h.rootFreeFor(k) == "" {
out = append(out, k.Kind)
}
}
@@ -177,6 +185,32 @@ func (h *Holder) carriers(level asks.Level) []string {
return out
}
// rootFreeFor is why a kind's proof is not believed now, or "": an agent can become root without a person
// where the router or the kind's holder runs, which the controller's self-check says as an open condition
// (novox/hq ADR 0259 §8, rule 2).
func (h *Holder) rootFreeFor(k KindInfo) string {
h.mu.Lock()
defer h.mu.Unlock()
for _, r := range h.open {
if r.Kind != KindAgentRoot || !r.Cleared.IsZero() {
continue
}
machine := strings.TrimPrefix(strings.TrimSuffix(r.Key, "."+KindAgentRoot), "machine.")
if machine == k.Node || (h.Node != "" && machine == h.Node) {
return "a program working for you can become root on " + machine + " without asking you, so an answer " +
"proven there proves nothing until that changes"
}
}
return ""
}
// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which
// raises the conditions.
const ConditionOwner = "mesh-controller"
// KindAgentRoot is the controller's condition while an agent can become root without a person on a machine.
const KindAgentRoot = "agent-root"
// TakeAsk takes one ask or cancel from the seat's queue. asker is the subject's last token, which the bus
// grants that module alone. An error is returned only when the ask could not be recorded, so the queue
// offers it again; a refusal is said to the asker and the work is taken.
@@ -257,8 +291,10 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
// Shown on the condition it is about, when that is open here; otherwise as a message of its own.
h.mu.Lock()
target := h.open["ask."+key]
if a.About != "" {
if r := h.open[a.About]; r != nil && r.Cleared.IsZero() {
// Shown on a condition's message only when asked by the condition's owner: another asker naming a
// condition's key does not take its message over.
if a.About != "" && asker == ConditionOwner {
if r := h.open[a.About]; r != nil && r.Cleared.IsZero() && r.Origin == "condition" {
target = r
}
}
@@ -416,6 +452,13 @@ func (h *Holder) closeAsk(key string, outcome asks.Outcome, words string, w *ask
rev = newRev
}
h.keepAsk(key, rec, rev)
if rec.Asker == SelfAsker {
// The router's own ask: it does what was decided, and tells nobody else (selfact.go).
h.markTold(key)
h.showClosed(rec)
h.selfAct(rec, *w)
return nil
}
if h.tell(*w) == nil {
h.markTold(key)
}
@@ -590,7 +633,7 @@ func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
return h.refuseChoice(kind, c, "it names an answer the question does not offer")
}
info, held := h.Bench.Kind(kind)
if !held || !h.Bench.Promises(kind, "choice") {
if !held || !h.Bench.Promises(kind, "choice") || !h.Bench.Promises(kind, "exact-render") {
return h.refuseChoice(kind, c, "this channel is not one the mesh holds for answers")
}
person := asks.Person{Who: asks.Operator, Kind: kind, Identity: c.Sender.Identity, Display: c.Sender.Display}
@@ -601,6 +644,14 @@ func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
return h.refuseChoice(kind, c, "the service did not authenticate who sent it")
}
id = h.identity(kind, c.Sender.Identity)
if rec.Only != nil {
// An ask put to one account is answered by that account alone, linked or not any more.
if rec.Only.Kind != kind || rec.Only.Identity != c.Sender.Identity {
return h.refuseChoice(kind, c, "this question was put to another account")
}
only := *rec.Only
id = &only
}
if id == nil {
return h.refuseChoice(kind, c, "this account is not linked as the operator")
}
@@ -619,6 +670,9 @@ func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
return h.refuseChoice(kind, c, "this account was linked at "+id.Linked.In(h.zone()).Format("15:04")+
" and can approve from "+id.ApproveFrom.In(h.zone()).Format("15:04"))
}
if why := h.rootFreeFor(info); why != "" {
return h.refuseChoice(kind, c, why)
}
proofs = append(proofs, "P1")
default:
return h.refuseChoice(kind, c, "it needs a code from the authenticator app, which the mesh does not take yet")
@@ -645,6 +699,30 @@ func (h *Holder) refuseChoice(kind string, c asks.Choice, why string) (string, e
return "refused", errorf("%s", why)
}
// showOnHandle is showOn, answering the handle the message is known by.
func (h *Holder) showOnHandle(kind string, m Message) (string, error) {
for _, ch := range h.channels() {
if ch.Name() == kind {
id, err := ch.Send(m)
h.noteChannel(kind, err)
return id, err
}
}
if h.Publish == nil {
return "", errors.New("this router cannot reach the bus")
}
out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To}
if out.To == "" && h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(asks.ChannelSubject(asks.Show, kind), out, out.Handle)
h.noteChannel(kind, err)
return out.Handle, err
}
// showOn says a message of the router's own on one kind, at once: an answer's refusal, details, a link's code.
func (h *Holder) showOn(kind string, m Message) error {
for _, ch := range h.channels() {
@@ -661,6 +739,12 @@ func (h *Holder) showOn(kind string, m Message) error {
if h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
if m.To != "" {
out.To = m.To
}
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(asks.ChannelSubject(asks.Send, kind), out, out.Handle)
h.noteChannel(kind, err)
return err