]> git.giacomolli.uk - onechat/blobdiff - notify.go
Share general review and documentation rules
[onechat] / notify.go
index 1036312c176d2983a7f1b5e8d15284f2be8b9089..d4e4e7a68b10e70149d7ea0a775155b23e1ace6a 100644 (file)
--- a/notify.go
+++ b/notify.go
@@ -3,6 +3,7 @@ package main
 import (
        "context"
        "crypto/sha256"
 import (
        "context"
        "crypto/sha256"
+       "encoding/hex"
        "encoding/json"
        "errors"
        "fmt"
        "encoding/json"
        "errors"
        "fmt"
@@ -36,33 +37,40 @@ const (
        // time.
        notifyTimeout = 10 * time.Second
 
        // time.
        notifyTimeout = 10 * time.Second
 
-       notifyUsage = `Usage: onechat notify --source SOURCE --id ID --text TEXT
+       notifyUsage = `Usage: onechat notify [--agent-id ID] --source SOURCE --id ID --text TEXT
 
 
-Send untrusted external data to the root agent at its next turn boundary.
+Send untrusted external data at the selected agent's next turn boundary.
+Omit --agent-id for root. An explicit ID must identify the root or a permanent
+child, not a task worker. Names are not accepted.
 Success means durably accepted, not processed. Retry with the same source,
 Success means durably accepted, not processed. Retry with the same source,
-ID, and text after an uncertain result. Changed text for an existing key fails.
+ID, target identity, and text after an uncertain result. A changed target or
+text for an existing key fails.
 Text is limited to 32768 bytes. Source and ID are 1 to 128 ASCII letters,
 digits, dots, underscores, or hyphens. The local test interface keeps up to
 1024 receipts without expiry and rejects new keys when full.
 Text is limited to 32768 bytes. Source and ID are 1 to 128 ASCII letters,
 digits, dots, underscores, or hyphens. The local test interface keeps up to
 1024 receipts without expiry and rejects new keys when full.
-Child targets, receipt expiry, and processing status are not supported.
+Receipt expiry and processing status are not supported.
 `
 )
 
 // NotifyInput is caller-supplied data. Its source label proves no identity or
 `
 )
 
 // NotifyInput is caller-supplied data. Its source label proves no identity or
-// authority. Source and ID together identify a retry of the same text.
+// authority. Source and ID together identify a retry of the same text. AgentID
+// selects a permanent agent. An empty AgentID selects the root.
 type NotifyInput struct {
 type NotifyInput struct {
-       Source string `json:"source"`
-       ID     string `json:"id"`
-       Text   string `json:"text"`
+       AgentID string `json:"agent_id,omitempty"`
+       Source  string `json:"source"`
+       ID      string `json:"id"`
+       Text    string `json:"text"`
 }
 
 // NotifyReceipt keeps a retry key after its queued delivery finishes. Digest
 }
 
 // NotifyReceipt keeps a retry key after its queued delivery finishes. Digest
-// binds the original text without retaining another copy of that text.
+// binds the original text without retaining another copy of that text. TargetID
+// binds the permanent agent identity. An empty TargetID selects the root.
 type NotifyReceipt struct {
 type NotifyReceipt struct {
-       Source string `json:"source"`
-       ID     string `json:"id"`
-       Digest string `json:"digest"`
-       Number int    `json:"number"`
+       TargetID string `json:"target_id,omitempty"`
+       Source   string `json:"source"`
+       ID       string `json:"id"`
+       Digest   string `json:"digest"`
+       Number   int    `json:"number"`
 }
 
 // notify submits a local event and prints only its acceptance receipt. The
 }
 
 // notify submits a local event and prints only its acceptance receipt. The
@@ -166,9 +174,10 @@ func notifyParse(args []string) (NotifyInput, error) {
        var input NotifyInput
 
        fields := map[string]*string{
        var input NotifyInput
 
        fields := map[string]*string{
-               "--source": &input.Source,
-               "--id":     &input.ID,
-               "--text":   &input.Text,
+               "--agent-id": &input.AgentID,
+               "--source":   &input.Source,
+               "--id":       &input.ID,
+               "--text":     &input.Text,
        }
 
        seen := make(map[string]bool)
        }
 
        seen := make(map[string]bool)
@@ -192,6 +201,10 @@ func notifyParse(args []string) (NotifyInput, error) {
                *field = value
        }
 
                *field = value
        }
 
+       if seen["--agent-id"] && input.AgentID == "" {
+               return input, errors.New("notification agent ID must not be empty")
+       }
+
        err := notifyValidate(input)
        if err != nil {
                return input, fmt.Errorf("validate notification: %w", err)
        err := notifyValidate(input)
        if err != nil {
                return input, fmt.Errorf("validate notification: %w", err)
@@ -202,6 +215,13 @@ func notifyParse(args []string) (NotifyInput, error) {
 
 // notifyValidate checks both CLI and direct socket inputs without echoing data.
 func notifyValidate(input NotifyInput) error {
 
 // notifyValidate checks both CLI and direct socket inputs without echoing data.
 func notifyValidate(input NotifyInput) error {
+       if input.AgentID != "" {
+               id, err := hex.DecodeString(input.AgentID)
+               if err != nil || len(id) != agentIDBytes {
+                       return errors.New("notification agent ID is invalid")
+               }
+       }
+
        if !notifyIdentifier(input.Source) || !notifyIdentifier(input.ID) {
                return errors.New("notification source and ID must be 1 to 128 ASCII letters, digits, dots, underscores, or hyphens")
        }
        if !notifyIdentifier(input.Source) || !notifyIdentifier(input.ID) {
                return errors.New("notification source and ID must be 1 to 128 ASCII letters, digits, dots, underscores, or hyphens")
        }