Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: hard remove maep

Lyric a06501ce 740df9a6

+222 -9104
-1
README.md
··· 26 26 What makes this project worth looking at: 27 27 28 28 - 🧩 **Reusable Go core**: Run the agent as a CLI, or embed it as a library/subprocess in other apps. 29 - - 🤝 **Mesh Agent Exchange Protocol (MAEP)**: You and your amigos run multiple agents and want them to message each other: use the MAEP, a p2p protocol with trust-state and audit trails. (see [docs/maep.md](docs/maep.md), WIP). 30 29 - 🔒 **Serious secure defaults**: Profile-based credential injection, Guard redaction, outbound policy controls, and async approvals with audit trails (see [docs/security.md](docs/security.md)). 31 30 - 🧰 **Practical Skills system**: Discover + inject `SKILL.md` from `file_state_dir/skills`, with simple on/off control (see [docs/skills.md](docs/skills.md)). 32 31 - 📚 **Beginner-friendly**: Built as a learning-first agent project, with detailed design docs in `docs/` and practical debugging tools like `--inspect-prompt` and `--inspect-request`.
+1 -12
assets/config/config.example.yaml
··· 205 205 enabled: true 206 206 max_items: 50 207 207 208 - # MAEP (Mesh Agent Exchange Protocol) local state. 209 - maep: 210 - listen_addrs: ["/ip4/0.0.0.0/tcp/4021"] 211 - 212 208 # Message bus 213 209 bus: 214 210 # In-process bus (M1). ··· 219 215 # Directory name under file_state_dir for contacts domain storage. 220 216 dir_name: "contacts" 221 217 proactive: 222 - # Max turns for one MAEP conversation session (peer_id + conversation_id). 218 + # Max turns for one proactive conversation session (contact + conversation_id). 223 219 max_turns_per_session: 6 224 220 # Cooldown after reaching max turns before auto-reply can resume. 225 221 session_cooldown: "10m" ··· 248 244 auth_token: "" 249 245 # Max queued tasks (in-memory). 250 246 max_queue: 100 251 - # If true, `mistermorph serve` also starts an embedded MAEP listener. 252 - # Can be overridden by CLI flag --with-maep. 253 - with_maep: false 254 - 255 247 # Console mode (`mistermorph console serve`). 256 248 console: 257 249 # Bind address for console API + SPA. ··· 280 272 # Telegram bot mode (`mistermorph telegram`). 281 273 telegram: 282 274 # Bot token from @BotFather. Prefer env var: MISTER_MORPH_TELEGRAM_BOT_TOKEN 283 - # If true, `mistermorph telegram` also starts an embedded MAEP listener. 284 - # Can be overridden by CLI flag --with-maep. 285 - with_maep: false 286 275 bot_token: "" 287 276 # Optional allowlist of chat ids (strings). If empty, allows all. 288 277 allowed_chat_ids: []
+13 -10
assets/config/contacts/ACTIVE.md
··· 30 30 last_interaction_at: "1970-01-01T00:00:00Z" # RFC3339 format 31 31 ``` 32 32 33 - ## Example Agent Contact 33 + ## Example Slack Contact 34 34 35 35 ```yaml 36 - contact_id: "maep:12D3KooWabc123xyz001" 37 - nickname: "Miss Morph" 38 - kind: "agent" 39 - channel: "maep" 40 - maep_node_id: "maep:12D3KooWabc123xyz001" 41 - maep_dial_address: "/ip4/..." 42 - persona_brief: "A mysterious agent with a knack for gathering intelligence and solving complex problems" 36 + contact_id: "slack:T12345:U67890" 37 + nickname: "Jane Doe" 38 + kind: "human" 39 + channel: "slack" 40 + slack_team_id: "T12345" 41 + slack_user_id: "U67890" 42 + slack_dm_channel_id: "D024BE91L" 43 + slack_channel_ids: 44 + - "C024BE91L" 45 + persona_brief: "Product manager focusing on roadmap coordination" 43 46 topic_preferences: 44 - - "espionage" 47 + - "product" 45 48 - "technology" 46 - - "mystery" 49 + - "planning" 47 50 cooldown_until: "1970-01-01T00:00:00Z" 48 51 last_interaction_at: "1970-01-01T00:00:00Z" 49 52 ```
-102
cmd/mistermorph/contactscmd/contacts.go
··· 1 1 package contactscmd 2 2 3 3 import ( 4 - "fmt" 5 4 "strings" 6 5 "time" 7 6 8 7 "github.com/quailyquaily/mistermorph/contacts" 9 8 "github.com/quailyquaily/mistermorph/internal/pathutil" 10 9 "github.com/quailyquaily/mistermorph/internal/statepaths" 11 - "github.com/quailyquaily/mistermorph/maep" 12 10 "github.com/spf13/cobra" 13 11 "github.com/spf13/viper" 14 12 ) ··· 19 17 Short: "Manage business-layer contacts", 20 18 } 21 19 cmd.PersistentFlags().String("dir", "", "Contacts state directory (defaults to file_state_dir/contacts)") 22 - 23 - cmd.AddCommand(newSyncMAEPCmd()) 24 - return cmd 25 - } 26 - 27 - func newSyncMAEPCmd() *cobra.Command { 28 - var maepDir string 29 - cmd := &cobra.Command{ 30 - Use: "sync-maep", 31 - Short: "Import MAEP contacts into contacts business store", 32 - RunE: func(cmd *cobra.Command, args []string) error { 33 - maepSvc := maepServiceFromDir(maepDir) 34 - maepContacts, err := maepSvc.ListContacts(cmd.Context()) 35 - if err != nil { 36 - return err 37 - } 38 - svc := serviceFromCmd(cmd) 39 - existingContacts, err := svc.ListContacts(cmd.Context(), "") 40 - if err != nil { 41 - return err 42 - } 43 - existingByNodeID := map[string]string{} 44 - for _, item := range existingContacts { 45 - nodeID := strings.TrimSpace(item.MAEPNodeID) 46 - contactID := strings.TrimSpace(item.ContactID) 47 - if nodeID == "" || contactID == "" { 48 - continue 49 - } 50 - if _, ok := existingByNodeID[nodeID]; ok { 51 - continue 52 - } 53 - existingByNodeID[nodeID] = contactID 54 - } 55 - 56 - now := time.Now().UTC() 57 - imported := 0 58 - for _, item := range maepContacts { 59 - nodeID := strings.TrimSpace(item.NodeID) 60 - targetContactID := resolveSyncMAEPTargetContactID(existingByNodeID, nodeID, item.PeerID) 61 - dialAddress := "" 62 - if len(item.Addresses) > 0 { 63 - dialAddress = strings.TrimSpace(item.Addresses[0]) 64 - } 65 - record := contacts.Contact{ 66 - ContactID: targetContactID, 67 - Kind: contacts.KindAgent, 68 - Channel: contacts.ChannelMAEP, 69 - ContactNickname: strings.TrimSpace(item.DisplayName), 70 - MAEPNodeID: nodeID, 71 - MAEPDialAddress: dialAddress, 72 - } 73 - updated, err := svc.UpsertContact(cmd.Context(), record, now) 74 - if err != nil { 75 - return err 76 - } 77 - if nodeID != "" { 78 - existingByNodeID[nodeID] = strings.TrimSpace(updated.ContactID) 79 - } 80 - imported++ 81 - } 82 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "imported: %d\n", imported) 83 - return nil 84 - }, 85 - } 86 - cmd.Flags().StringVar(&maepDir, "maep-dir", "", "MAEP state directory (defaults to file_state_dir/maep)") 87 20 return cmd 88 21 } 89 22 ··· 110 43 } 111 44 return v 112 45 } 113 - 114 - func maepServiceFromDir(dir string) *maep.Service { 115 - dir = strings.TrimSpace(dir) 116 - if dir == "" { 117 - dir = statepaths.MAEPDir() 118 - } else { 119 - dir = pathutil.ExpandHomePath(dir) 120 - } 121 - return maep.NewService(maep.NewFileStore(dir)) 122 - } 123 - 124 - func chooseContactID(nodeID string, peerID string) string { 125 - nodeID = strings.TrimSpace(nodeID) 126 - if nodeID != "" { 127 - return nodeID 128 - } 129 - peerID = strings.TrimSpace(peerID) 130 - if peerID == "" { 131 - return "" 132 - } 133 - return "maep:" + peerID 134 - } 135 - 136 - func resolveSyncMAEPTargetContactID(existingByNodeID map[string]string, nodeID string, peerID string) string { 137 - nodeID = strings.TrimSpace(nodeID) 138 - if nodeID != "" && existingByNodeID != nil { 139 - if existing, ok := existingByNodeID[nodeID]; ok { 140 - existing = strings.TrimSpace(existing) 141 - if existing != "" { 142 - return existing 143 - } 144 - } 145 - } 146 - return chooseContactID(nodeID, peerID) 147 - }
-25
cmd/mistermorph/contactscmd/sync_maep_test.go
··· 1 - package contactscmd 2 - 3 - import "testing" 4 - 5 - func TestResolveSyncMAEPTargetContactID_PrefersExistingNodeID(t *testing.T) { 6 - existingByNodeID := map[string]string{ 7 - "maep:node-1": "custom-contact-id", 8 - } 9 - got := resolveSyncMAEPTargetContactID(existingByNodeID, "maep:node-1", "12D3KooWPeer") 10 - if got != "custom-contact-id" { 11 - t.Fatalf("resolveSyncMAEPTargetContactID() = %q, want %q", got, "custom-contact-id") 12 - } 13 - } 14 - 15 - func TestResolveSyncMAEPTargetContactID_FallsBackToCanonical(t *testing.T) { 16 - got := resolveSyncMAEPTargetContactID(nil, "maep:node-2", "12D3KooWPeer") 17 - if got != "maep:node-2" { 18 - t.Fatalf("resolveSyncMAEPTargetContactID() = %q, want %q", got, "maep:node-2") 19 - } 20 - 21 - got = resolveSyncMAEPTargetContactID(nil, "", "12D3KooWPeer") 22 - if got != "maep:12D3KooWPeer" { 23 - t.Fatalf("resolveSyncMAEPTargetContactID() = %q, want %q", got, "maep:12D3KooWPeer") 24 - } 25 - }
-20
cmd/mistermorph/daemoncmd/serve.go
··· 19 19 "github.com/quailyquaily/mistermorph/internal/llmconfig" 20 20 "github.com/quailyquaily/mistermorph/internal/llmutil" 21 21 "github.com/quailyquaily/mistermorph/internal/logutil" 22 - "github.com/quailyquaily/mistermorph/internal/maepruntime" 23 22 "github.com/quailyquaily/mistermorph/internal/outputfmt" 24 23 "github.com/quailyquaily/mistermorph/internal/promptprofile" 25 24 "github.com/quailyquaily/mistermorph/internal/skillsutil" 26 25 "github.com/quailyquaily/mistermorph/internal/statepaths" 27 26 "github.com/quailyquaily/mistermorph/internal/toolsutil" 28 27 "github.com/quailyquaily/mistermorph/llm" 29 - "github.com/quailyquaily/mistermorph/maep" 30 28 "github.com/quailyquaily/mistermorph/tools" 31 29 "github.com/spf13/cobra" 32 30 "github.com/spf13/viper" ··· 59 57 return err 60 58 } 61 59 slog.SetDefault(logger) 62 - withMAEP := configutil.FlagOrViperBool(cmd, "with-maep", "server.with_maep") 63 - if withMAEP { 64 - maepListenAddrs := configutil.FlagOrViperStringArray(cmd, "maep-listen", "maep.listen_addrs") 65 - maepNode, err := maepruntime.Start(cmd.Context(), maepruntime.StartOptions{ 66 - ListenAddrs: maepListenAddrs, 67 - Logger: logger, 68 - OnDataPush: func(event maep.DataPushEvent) { 69 - logger.Info("daemon_maep_data_push", "from_peer_id", event.FromPeerID, "topic", event.Topic, "deduped", event.Deduped) 70 - }, 71 - }) 72 - if err != nil { 73 - return fmt.Errorf("start embedded maep: %w", err) 74 - } 75 - defer maepNode.Close() 76 - logger.Info("daemon_maep_ready", "peer_id", maepNode.PeerID(), "addresses", maepNode.AddrStrings()) 77 - } 78 60 79 61 llmValues := llmutil.RuntimeValuesFromViper() 80 62 provider := strings.TrimSpace(llmValues.Provider) ··· 440 422 cmd.Flags().String("server-listen", "127.0.0.1:8787", "HTTP listen address (host:port).") 441 423 cmd.Flags().String("server-auth-token", "", "Bearer token required for all non-/health endpoints.") 442 424 cmd.Flags().Int("server-max-queue", 100, "Max queued tasks in memory.") 443 - cmd.Flags().Bool("with-maep", false, "Start MAEP listener together with daemon serve.") 444 - cmd.Flags().StringArray("maep-listen", nil, "MAEP listen multiaddr for --with-maep (repeatable). Defaults to maep.listen_addrs or MAEP defaults.") 445 425 446 426 return cmd 447 427 }
-6
cmd/mistermorph/defaults.go
··· 36 36 viper.SetDefault("skills.mode", "on") 37 37 viper.SetDefault("skills.dir_name", "skills") 38 38 39 - // MAEP 40 - viper.SetDefault("maep.dir_name", "maep") 41 - viper.SetDefault("maep.listen_addrs", []string{}) 42 - 43 39 // Bus 44 40 viper.SetDefault("bus.max_inflight", 1024) 45 41 ··· 51 47 // Daemon server 52 48 viper.SetDefault("server.listen", "127.0.0.1:8787") 53 49 viper.SetDefault("server.max_queue", 100) 54 - viper.SetDefault("server.with_maep", false) 55 50 56 51 // Console server 57 52 viper.SetDefault("console.enabled", true) ··· 73 68 viper.SetDefault("telegram.addressing_confidence_threshold", 0.6) 74 69 viper.SetDefault("telegram.addressing_interject_threshold", 0.6) 75 70 viper.SetDefault("telegram.max_concurrency", 3) 76 - viper.SetDefault("telegram.with_maep", false) 77 71 78 72 // Slack 79 73 viper.SetDefault("slack.base_url", "https://slack.com/api")
-23
cmd/mistermorph/install.go
··· 1 1 package main 2 2 3 3 import ( 4 - "context" 5 4 "fmt" 6 5 "os" 7 6 "path/filepath" 8 7 "strings" 9 - "time" 10 8 11 9 "github.com/quailyquaily/mistermorph/assets" 12 10 "github.com/quailyquaily/mistermorph/cmd/mistermorph/skillscmd" 13 11 "github.com/quailyquaily/mistermorph/internal/clifmt" 14 12 "github.com/quailyquaily/mistermorph/internal/pathutil" 15 - "github.com/quailyquaily/mistermorph/maep" 16 13 "github.com/spf13/cobra" 17 14 "github.com/spf13/viper" 18 15 ) ··· 210 207 fmt.Printf("%s: %d files\n", clifmt.Success("done"), len(filePlans)) 211 208 } 212 209 213 - identity, created, err := ensureInstallMAEPIdentity(dir) 214 - if err != nil { 215 - return fmt.Errorf("initialize maep identity: %w", err) 216 - } 217 - if created { 218 - fmt.Printf("[i] maep identity created: %s\n", identity.PeerID) 219 - } else { 220 - fmt.Printf("[i] maep identity exists: %s\n", identity.PeerID) 221 - } 222 - 223 210 skillsDir := filepath.Join(dir, "skills") 224 211 skillDirs, err := skillscmd.DiscoverBuiltInSkills() 225 212 if err != nil { ··· 353 340 cfg = applyInstallConfigSetupOverrides(cfg, setup) 354 341 return cfg 355 342 } 356 - 357 - func ensureInstallMAEPIdentity(dir string) (maep.Identity, bool, error) { 358 - maepDirName := strings.TrimSpace(viper.GetString("maep.dir_name")) 359 - if maepDirName == "" { 360 - maepDirName = "maep" 361 - } 362 - maepDir := filepath.Join(dir, maepDirName) 363 - svc := maep.NewService(maep.NewFileStore(maepDir)) 364 - return svc.EnsureIdentity(context.Background(), time.Now().UTC()) 365 - }
-68
cmd/mistermorph/install_test.go
··· 99 99 } 100 100 } 101 101 102 - func TestEnsureInstallMAEPIdentity_ReusesExistingIdentity(t *testing.T) { 103 - initViperDefaults() 104 - originalDirName := viper.GetString("maep.dir_name") 105 - viper.Set("maep.dir_name", "maep") 106 - t.Cleanup(func() { 107 - if originalDirName == "" { 108 - viper.Set("maep.dir_name", nil) 109 - return 110 - } 111 - viper.Set("maep.dir_name", originalDirName) 112 - }) 113 - 114 - root := t.TempDir() 115 - 116 - first, created, err := ensureInstallMAEPIdentity(root) 117 - if err != nil { 118 - t.Fatalf("ensureInstallMAEPIdentity() first call error = %v", err) 119 - } 120 - if !created { 121 - t.Fatalf("ensureInstallMAEPIdentity() first call expected created=true") 122 - } 123 - 124 - second, created, err := ensureInstallMAEPIdentity(root) 125 - if err != nil { 126 - t.Fatalf("ensureInstallMAEPIdentity() second call error = %v", err) 127 - } 128 - if created { 129 - t.Fatalf("ensureInstallMAEPIdentity() second call expected created=false") 130 - } 131 - if second.PeerID != first.PeerID { 132 - t.Fatalf("peer_id changed after second init: got %s want %s", second.PeerID, first.PeerID) 133 - } 134 - if second.NodeUUID != first.NodeUUID { 135 - t.Fatalf("node_uuid changed after second init: got %s want %s", second.NodeUUID, first.NodeUUID) 136 - } 137 - if second.NodeID != first.NodeID { 138 - t.Fatalf("node_id changed after second init: got %s want %s", second.NodeID, first.NodeID) 139 - } 140 - 141 - identityPath := filepath.Join(root, "maep", "identity.json") 142 - if _, err := os.Stat(identityPath); err != nil { 143 - t.Fatalf("identity file missing at %s: %v", identityPath, err) 144 - } 145 - } 146 - 147 - func TestEnsureInstallMAEPIdentity_RespectsMaepDirName(t *testing.T) { 148 - initViperDefaults() 149 - originalDirName := viper.GetString("maep.dir_name") 150 - viper.Set("maep.dir_name", "custom-maep") 151 - t.Cleanup(func() { 152 - if originalDirName == "" { 153 - viper.Set("maep.dir_name", nil) 154 - return 155 - } 156 - viper.Set("maep.dir_name", originalDirName) 157 - }) 158 - 159 - root := t.TempDir() 160 - if _, _, err := ensureInstallMAEPIdentity(root); err != nil { 161 - t.Fatalf("ensureInstallMAEPIdentity() error = %v", err) 162 - } 163 - 164 - identityPath := filepath.Join(root, "custom-maep", "identity.json") 165 - if _, err := os.Stat(identityPath); err != nil { 166 - t.Fatalf("identity file missing at %s: %v", identityPath, err) 167 - } 168 - } 169 - 170 102 func TestLoadIdentityTemplate(t *testing.T) { 171 103 body, err := loadIdentityTemplate() 172 104 if err != nil {
-159
cmd/mistermorph/maepcmd/card_export_test.go
··· 1 - package maepcmd 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "net" 7 - "path/filepath" 8 - "strings" 9 - "testing" 10 - "time" 11 - 12 - "github.com/quailyquaily/mistermorph/maep" 13 - ) 14 - 15 - func TestResolveCardExportAddresses_UsesConfiguredListenAddrs(t *testing.T) { 16 - ctx := context.Background() 17 - svc := maep.NewService(maep.NewFileStore(filepath.Join(t.TempDir(), "maep"))) 18 - identity, _, err := svc.EnsureIdentity(ctx, time.Now().UTC()) 19 - if err != nil { 20 - t.Fatalf("EnsureIdentity() error = %v", err) 21 - } 22 - 23 - got, err := resolveCardExportAddresses(ctx, svc, nil, []string{"/ip4/127.0.0.1/tcp/4101"}) 24 - if err != nil { 25 - t.Fatalf("resolveCardExportAddresses() error = %v", err) 26 - } 27 - if len(got) != 1 { 28 - t.Fatalf("resolveCardExportAddresses() len=%d want=1", len(got)) 29 - } 30 - wantSuffix := "/p2p/" + identity.PeerID 31 - if !strings.HasSuffix(got[0], wantSuffix) { 32 - t.Fatalf("resolved address missing peer suffix: got %q want suffix %q", got[0], wantSuffix) 33 - } 34 - } 35 - 36 - func TestResolveCardExportAddresses_ConfigPeerIDMismatch(t *testing.T) { 37 - ctx := context.Background() 38 - svc := maep.NewService(maep.NewFileStore(filepath.Join(t.TempDir(), "maep"))) 39 - if _, _, err := svc.EnsureIdentity(ctx, time.Now().UTC()); err != nil { 40 - t.Fatalf("EnsureIdentity() error = %v", err) 41 - } 42 - otherSvc := maep.NewService(maep.NewFileStore(filepath.Join(t.TempDir(), "maep-other"))) 43 - otherIdentity, _, err := otherSvc.EnsureIdentity(ctx, time.Now().UTC()) 44 - if err != nil { 45 - t.Fatalf("EnsureIdentity(other) error = %v", err) 46 - } 47 - 48 - _, err = resolveCardExportAddresses(ctx, svc, nil, []string{"/ip4/127.0.0.1/tcp/4101/p2p/" + otherIdentity.PeerID}) 49 - if err == nil { 50 - t.Fatalf("resolveCardExportAddresses() expected error, got nil") 51 - } 52 - if !strings.Contains(err.Error(), "mismatched /p2p/") { 53 - t.Fatalf("unexpected error: %v", err) 54 - } 55 - } 56 - 57 - func TestResolveCardExportAddresses_ExplicitAddressesTakePriority(t *testing.T) { 58 - ctx := context.Background() 59 - svc := maep.NewService(maep.NewFileStore(filepath.Join(t.TempDir(), "maep"))) 60 - if _, _, err := svc.EnsureIdentity(ctx, time.Now().UTC()); err != nil { 61 - t.Fatalf("EnsureIdentity() error = %v", err) 62 - } 63 - 64 - explicit := []string{"/ip4/127.0.0.1/tcp/4102/p2p/12D3KooWExplicitPeer"} 65 - got, err := resolveCardExportAddresses(ctx, svc, explicit, []string{"/ip4/127.0.0.1/tcp/4101"}) 66 - if err != nil { 67 - t.Fatalf("resolveCardExportAddresses() error = %v", err) 68 - } 69 - if len(got) != 1 || got[0] != explicit[0] { 70 - t.Fatalf("explicit addresses not preserved: got=%v want=%v", got, explicit) 71 - } 72 - } 73 - 74 - func TestResolveCardExportAddresses_InteractiveSelection(t *testing.T) { 75 - ctx := context.Background() 76 - svc := maep.NewService(maep.NewFileStore(filepath.Join(t.TempDir(), "maep"))) 77 - identity, _, err := svc.EnsureIdentity(ctx, time.Now().UTC()) 78 - if err != nil { 79 - t.Fatalf("EnsureIdentity() error = %v", err) 80 - } 81 - 82 - in := strings.NewReader("2\n") 83 - var out bytes.Buffer 84 - got, err := resolveCardExportAddressesWithPrompt( 85 - ctx, 86 - svc, 87 - nil, 88 - []string{"/ip4/127.0.0.1/tcp/4101", "/ip4/127.0.0.1/tcp/4102"}, 89 - in, 90 - &out, 91 - true, 92 - ) 93 - if err != nil { 94 - t.Fatalf("resolveCardExportAddressesWithPrompt() error = %v", err) 95 - } 96 - if len(got) != 1 { 97 - t.Fatalf("resolveCardExportAddressesWithPrompt() len=%d want=1", len(got)) 98 - } 99 - want := "/ip4/127.0.0.1/tcp/4102/p2p/" + identity.PeerID 100 - if got[0] != want { 101 - t.Fatalf("unexpected selected address: got=%q want=%q", got[0], want) 102 - } 103 - if !strings.Contains(out.String(), "Select one dialable address") { 104 - t.Fatalf("expected prompt output, got=%q", out.String()) 105 - } 106 - } 107 - 108 - func TestResolveCardExportAddresses_NonInteractiveMultipleValidRequiresExplicitAddress(t *testing.T) { 109 - ctx := context.Background() 110 - svc := maep.NewService(maep.NewFileStore(filepath.Join(t.TempDir(), "maep"))) 111 - if _, _, err := svc.EnsureIdentity(ctx, time.Now().UTC()); err != nil { 112 - t.Fatalf("EnsureIdentity() error = %v", err) 113 - } 114 - 115 - _, err := resolveCardExportAddressesWithPrompt( 116 - ctx, 117 - svc, 118 - nil, 119 - []string{"/ip4/127.0.0.1/tcp/4101", "/ip4/127.0.0.1/tcp/4102"}, 120 - nil, 121 - nil, 122 - false, 123 - ) 124 - if err == nil { 125 - t.Fatalf("resolveCardExportAddressesWithPrompt() expected error, got nil") 126 - } 127 - if !strings.Contains(err.Error(), "multiple dialable addresses found") { 128 - t.Fatalf("unexpected error: %v", err) 129 - } 130 - } 131 - 132 - func TestExpandConfiguredDialAddressesWithIPs_ReplacesUnspecifiedIPv4(t *testing.T) { 133 - input := []string{ 134 - "/ip4/0.0.0.0/tcp/4021", 135 - } 136 - out := expandConfiguredDialAddressesWithIPs(input, []net.IP{ 137 - net.ParseIP("192.168.1.10"), 138 - net.ParseIP("127.0.0.1"), 139 - }) 140 - if len(out) < 2 { 141 - t.Fatalf("expandConfiguredDialAddressesWithIPs() len=%d want>=2", len(out)) 142 - } 143 - foundLAN := false 144 - foundLoopback := false 145 - for _, addr := range out { 146 - if strings.Contains(addr, "/ip4/192.168.1.10/") { 147 - foundLAN = true 148 - } 149 - if strings.Contains(addr, "/ip4/127.0.0.1/") { 150 - foundLoopback = true 151 - } 152 - } 153 - if !foundLAN { 154 - t.Fatalf("expected expanded LAN address, got=%v", out) 155 - } 156 - if !foundLoopback { 157 - t.Fatalf("expected expanded loopback address, got=%v", out) 158 - } 159 - }
-133
cmd/mistermorph/maepcmd/inbox_list_test.go
··· 1 - package maepcmd 2 - 3 - import ( 4 - "encoding/base64" 5 - "encoding/json" 6 - "strings" 7 - "testing" 8 - 9 - "github.com/quailyquaily/mistermorph/maep" 10 - ) 11 - 12 - func TestSummarizePayload_JSONNotTruncated(t *testing.T) { 13 - longText := strings.Repeat("x", 220) 14 - payload := map[string]any{ 15 - "message_id": "msg_1", 16 - "text": longText, 17 - "sent_at": "2026-02-07T04:31:30Z", 18 - } 19 - raw, _ := json.Marshal(payload) 20 - 21 - got := summarizePayload("application/json", base64.RawURLEncoding.EncodeToString(raw)) 22 - if strings.Contains(got, "...") { 23 - t.Fatalf("expected full payload without truncation, got %q", got) 24 - } 25 - if !strings.Contains(got, longText) { 26 - t.Fatalf("expected summarized payload to contain full text") 27 - } 28 - } 29 - 30 - func TestSummarizePayload_TextNotTruncated(t *testing.T) { 31 - longText := strings.Repeat("a", 220) 32 - got := summarizePayload("text/plain", base64.RawURLEncoding.EncodeToString([]byte(longText))) 33 - if got != longText { 34 - t.Fatalf("text payload mismatch: got len=%d want len=%d", len(got), len(longText)) 35 - } 36 - } 37 - 38 - func TestSummarizePayload_EnvelopePrettyJSON(t *testing.T) { 39 - payload := map[string]any{ 40 - "message_id": "msg_1", 41 - "text": "hello", 42 - "sent_at": "2026-02-07T04:31:30Z", 43 - } 44 - raw, _ := json.Marshal(payload) 45 - got := summarizePayload("application/json", base64.RawURLEncoding.EncodeToString(raw)) 46 - if !strings.Contains(got, "\n \"message_id\"") { 47 - t.Fatalf("expected pretty envelope json, got %q", got) 48 - } 49 - } 50 - 51 - func TestSummarizePayload_NonEnvelopeJSONCompact(t *testing.T) { 52 - payload := map[string]any{ 53 - "foo": "bar", 54 - } 55 - raw, _ := json.Marshal(payload) 56 - got := summarizePayload("application/json", base64.RawURLEncoding.EncodeToString(raw)) 57 - if strings.Contains(got, "\n") { 58 - t.Fatalf("expected compact json for non-envelope payload, got %q", got) 59 - } 60 - } 61 - 62 - func TestWriteInboxRecords_BlockLayout(t *testing.T) { 63 - payload := map[string]any{ 64 - "message_id": "msg_1", 65 - "text": "hello", 66 - "sent_at": "2026-02-07T04:31:30Z", 67 - } 68 - raw, _ := json.Marshal(payload) 69 - record := maep.InboxMessage{ 70 - FromPeerID: "12D3KooX", 71 - Topic: "dm.reply.v1", 72 - ContentType: "application/json", 73 - PayloadBase64: base64.RawURLEncoding.EncodeToString(raw), 74 - IdempotencyKey: "reply:1", 75 - SessionID: "12D3KooX::dialogue.v1", 76 - } 77 - var b strings.Builder 78 - writeInboxRecords(&b, []maep.InboxMessage{record}) 79 - out := b.String() 80 - for _, want := range []string{ 81 - "[1]\n", 82 - "from_peer_id: 12D3KooX\n", 83 - "topic: dm.reply.v1\n", 84 - "session_id: 12D3KooX::dialogue.v1\n", 85 - "content_type: application/json\n", 86 - "payload:\n", 87 - " {\n", 88 - } { 89 - if !strings.Contains(out, want) { 90 - t.Fatalf("writeInboxRecords() missing %q in output:\n%s", want, out) 91 - } 92 - } 93 - } 94 - 95 - func TestIndentBlock(t *testing.T) { 96 - got := indentBlock("a\nb", " ") 97 - if got != " a\n b" { 98 - t.Fatalf("indentBlock() mismatch: got %q", got) 99 - } 100 - } 101 - 102 - func TestWriteOutboxRecords_BlockLayout(t *testing.T) { 103 - payload := map[string]any{ 104 - "message_id": "msg_1", 105 - "text": "hello", 106 - "sent_at": "2026-02-07T04:31:30Z", 107 - } 108 - raw, _ := json.Marshal(payload) 109 - record := maep.OutboxMessage{ 110 - ToPeerID: "12D3KooY", 111 - Topic: "dm.reply.v1", 112 - ContentType: "application/json", 113 - PayloadBase64: base64.RawURLEncoding.EncodeToString(raw), 114 - IdempotencyKey: "reply:2", 115 - SessionID: "12D3KooY::dialogue.v1", 116 - } 117 - var b strings.Builder 118 - writeOutboxRecords(&b, []maep.OutboxMessage{record}) 119 - out := b.String() 120 - for _, want := range []string{ 121 - "[1]\n", 122 - "to_peer_id: 12D3KooY\n", 123 - "topic: dm.reply.v1\n", 124 - "session_id: 12D3KooY::dialogue.v1\n", 125 - "content_type: application/json\n", 126 - "payload:\n", 127 - " {\n", 128 - } { 129 - if !strings.Contains(out, want) { 130 - t.Fatalf("writeOutboxRecords() missing %q in output:\n%s", want, out) 131 - } 132 - } 133 - }
-1328
cmd/mistermorph/maepcmd/maep.go
··· 1 - package maepcmd 2 - 3 - import ( 4 - "bufio" 5 - "context" 6 - "encoding/base64" 7 - "encoding/json" 8 - "fmt" 9 - "io" 10 - "log/slog" 11 - "net" 12 - "os" 13 - "os/signal" 14 - "sort" 15 - "strconv" 16 - "strings" 17 - "syscall" 18 - "time" 19 - 20 - "github.com/google/uuid" 21 - ma "github.com/multiformats/go-multiaddr" 22 - "github.com/quailyquaily/mistermorph/contacts" 23 - busruntime "github.com/quailyquaily/mistermorph/internal/bus" 24 - maepbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/maep" 25 - "github.com/quailyquaily/mistermorph/internal/idempotency" 26 - "github.com/quailyquaily/mistermorph/internal/pathutil" 27 - "github.com/quailyquaily/mistermorph/internal/statepaths" 28 - "github.com/quailyquaily/mistermorph/maep" 29 - "github.com/spf13/cobra" 30 - "github.com/spf13/viper" 31 - ) 32 - 33 - func New() *cobra.Command { 34 - cmd := &cobra.Command{ 35 - Use: "maep", 36 - Short: "Manage MAEP identity, contacts, and P2P exchange", 37 - } 38 - cmd.PersistentFlags().String("dir", "", "MAEP state directory (defaults to file_state_dir/maep)") 39 - 40 - cmd.AddCommand(newInitCmd()) 41 - cmd.AddCommand(newIDCmd()) 42 - cmd.AddCommand(newCardCmd()) 43 - cmd.AddCommand(newContactsCmd()) 44 - cmd.AddCommand(newAuditCmd()) 45 - cmd.AddCommand(newInboxCmd()) 46 - cmd.AddCommand(newOutboxCmd()) 47 - cmd.AddCommand(newServeCmd()) 48 - cmd.AddCommand(newHelloCmd()) 49 - cmd.AddCommand(newPingCmd()) 50 - cmd.AddCommand(newCapabilitiesCmd()) 51 - cmd.AddCommand(newPushCmd()) 52 - return cmd 53 - } 54 - 55 - func newInitCmd() *cobra.Command { 56 - var outputJSON bool 57 - cmd := &cobra.Command{ 58 - Use: "init", 59 - Short: "Initialize local MAEP identity", 60 - RunE: func(cmd *cobra.Command, args []string) error { 61 - svc := serviceFromCmd(cmd) 62 - identity, created, err := svc.EnsureIdentity(cmd.Context(), time.Now().UTC()) 63 - if err != nil { 64 - return err 65 - } 66 - fingerprint, _ := maep.FingerprintGrouped(identity.IdentityPubEd25519) 67 - view := map[string]any{ 68 - "created": created, 69 - "node_uuid": identity.NodeUUID, 70 - "peer_id": identity.PeerID, 71 - "node_id": identity.NodeID, 72 - "fingerprint": fingerprint, 73 - } 74 - if outputJSON { 75 - return writeJSON(cmd.OutOrStdout(), view) 76 - } 77 - 78 - state := "existing" 79 - if created { 80 - state = "created" 81 - } 82 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "state: %s\nnode_uuid: %s\npeer_id: %s\nnode_id: %s\nfingerprint: %s\n", state, identity.NodeUUID, identity.PeerID, identity.NodeID, fingerprint) 83 - return nil 84 - }, 85 - } 86 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 87 - return cmd 88 - } 89 - 90 - func newIDCmd() *cobra.Command { 91 - var outputJSON bool 92 - cmd := &cobra.Command{ 93 - Use: "id", 94 - Short: "Show local MAEP identity", 95 - RunE: func(cmd *cobra.Command, args []string) error { 96 - svc := serviceFromCmd(cmd) 97 - identity, ok, err := svc.GetIdentity(cmd.Context()) 98 - if err != nil { 99 - return err 100 - } 101 - if !ok { 102 - return fmt.Errorf("identity not found; run `mistermorph maep init`") 103 - } 104 - 105 - fingerprint, _ := maep.FingerprintGrouped(identity.IdentityPubEd25519) 106 - view := map[string]any{ 107 - "node_uuid": identity.NodeUUID, 108 - "peer_id": identity.PeerID, 109 - "node_id": identity.NodeID, 110 - "identity_pub_ed25519": identity.IdentityPubEd25519, 111 - "fingerprint": fingerprint, 112 - "created_at": identity.CreatedAt, 113 - "updated_at": identity.UpdatedAt, 114 - } 115 - if outputJSON { 116 - return writeJSON(cmd.OutOrStdout(), view) 117 - } 118 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "node_uuid: %s\npeer_id: %s\nnode_id: %s\nidentity_pub_ed25519: %s\nfingerprint: %s\ncreated_at: %s\nupdated_at: %s\n", identity.NodeUUID, identity.PeerID, identity.NodeID, identity.IdentityPubEd25519, fingerprint, identity.CreatedAt.UTC().Format(time.RFC3339), identity.UpdatedAt.UTC().Format(time.RFC3339)) 119 - return nil 120 - }, 121 - } 122 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 123 - return cmd 124 - } 125 - 126 - func newCardCmd() *cobra.Command { 127 - cmd := &cobra.Command{ 128 - Use: "card", 129 - Short: "Manage MAEP contact cards", 130 - } 131 - cmd.AddCommand(newCardExportCmd()) 132 - return cmd 133 - } 134 - 135 - func newCardExportCmd() *cobra.Command { 136 - var addresses []string 137 - var outPath string 138 - var minProtocol int 139 - var maxProtocol int 140 - var expiresIn time.Duration 141 - 142 - cmd := &cobra.Command{ 143 - Use: "export", 144 - Short: "Export a signed contact card", 145 - RunE: func(cmd *cobra.Command, args []string) error { 146 - svc := serviceFromCmd(cmd) 147 - resolvedAddresses, err := resolveCardExportAddressesForCommand( 148 - cmd.Context(), 149 - svc, 150 - addresses, 151 - viper.GetStringSlice("maep.listen_addrs"), 152 - cmd.InOrStdin(), 153 - cmd.ErrOrStderr(), 154 - ) 155 - if err != nil { 156 - return err 157 - } 158 - var expiresAt *time.Time 159 - if expiresIn > 0 { 160 - t := time.Now().UTC().Add(expiresIn) 161 - expiresAt = &t 162 - } 163 - _, raw, err := svc.ExportContactCard(cmd.Context(), resolvedAddresses, minProtocol, maxProtocol, time.Now().UTC(), expiresAt) 164 - if err != nil { 165 - return err 166 - } 167 - if strings.TrimSpace(outPath) == "" { 168 - _, _ = cmd.OutOrStdout().Write(raw) 169 - return nil 170 - } 171 - path := pathutil.ExpandHomePath(strings.TrimSpace(outPath)) 172 - if err := os.WriteFile(path, raw, 0o600); err != nil { 173 - return err 174 - } 175 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "written: %s\n", path) 176 - return nil 177 - }, 178 - } 179 - 180 - cmd.Flags().StringArrayVar(&addresses, "address", nil, "Contact multiaddr (repeatable, must end with /p2p/<peer_id>; if omitted, choose one dialable address from maep.listen_addrs)") 181 - cmd.Flags().StringVar(&outPath, "out", "", "Output file path (default stdout)") 182 - cmd.Flags().IntVar(&minProtocol, "min-protocol", 1, "Minimum supported protocol version") 183 - cmd.Flags().IntVar(&maxProtocol, "max-protocol", 1, "Maximum supported protocol version") 184 - cmd.Flags().DurationVar(&expiresIn, "expires-in", 0, "Relative expiration duration, e.g. 720h (0 disables)") 185 - 186 - return cmd 187 - } 188 - 189 - func newContactsCmd() *cobra.Command { 190 - cmd := &cobra.Command{ 191 - Use: "contacts", 192 - Short: "Manage MAEP contacts", 193 - } 194 - cmd.AddCommand(newContactsListCmd()) 195 - cmd.AddCommand(newContactsImportCmd()) 196 - cmd.AddCommand(newContactsShowCmd()) 197 - cmd.AddCommand(newContactsVerifyCmd()) 198 - return cmd 199 - } 200 - 201 - func newContactsListCmd() *cobra.Command { 202 - var outputJSON bool 203 - cmd := &cobra.Command{ 204 - Use: "list", 205 - Short: "List contacts", 206 - RunE: func(cmd *cobra.Command, args []string) error { 207 - svc := serviceFromCmd(cmd) 208 - contacts, err := svc.ListContacts(cmd.Context()) 209 - if err != nil { 210 - return err 211 - } 212 - sort.Slice(contacts, func(i, j int) bool { 213 - if contacts[i].UpdatedAt.Equal(contacts[j].UpdatedAt) { 214 - return contacts[i].PeerID < contacts[j].PeerID 215 - } 216 - return contacts[i].UpdatedAt.After(contacts[j].UpdatedAt) 217 - }) 218 - 219 - if outputJSON { 220 - return writeJSON(cmd.OutOrStdout(), contacts) 221 - } 222 - if len(contacts) == 0 { 223 - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "no contacts") 224 - return nil 225 - } 226 - for _, c := range contacts { 227 - display := strings.TrimSpace(c.DisplayName) 228 - if display == "" { 229 - display = "-" 230 - } 231 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\t%s\t%s\n", c.PeerID, c.TrustState, c.NodeUUID, display) 232 - } 233 - return nil 234 - }, 235 - } 236 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 237 - return cmd 238 - } 239 - 240 - func newContactsImportCmd() *cobra.Command { 241 - var displayName string 242 - cmd := &cobra.Command{ 243 - Use: "import <contact_card.json|->", 244 - Short: "Import a contact card", 245 - Args: cobra.ExactArgs(1), 246 - RunE: func(cmd *cobra.Command, args []string) error { 247 - raw, err := readInputFile(args[0]) 248 - if err != nil { 249 - return err 250 - } 251 - svc := serviceFromCmd(cmd) 252 - result, err := svc.ImportContactCard(cmd.Context(), raw, displayName, time.Now().UTC()) 253 - if err != nil { 254 - symbol := maep.SymbolOf(err) 255 - if symbol != "" { 256 - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "symbol: %s\n", symbol) 257 - } 258 - return err 259 - } 260 - status := "updated" 261 - if result.Created { 262 - status = "created" 263 - } 264 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "status: %s\npeer_id: %s\nnode_uuid: %s\ntrust_state: %s\n", status, result.Contact.PeerID, result.Contact.NodeUUID, result.Contact.TrustState) 265 - return nil 266 - }, 267 - } 268 - cmd.Flags().StringVar(&displayName, "display-name", "", "Display name override for this contact") 269 - return cmd 270 - } 271 - 272 - func newContactsShowCmd() *cobra.Command { 273 - var outputJSON bool 274 - cmd := &cobra.Command{ 275 - Use: "show <peer_id>", 276 - Short: "Show contact detail", 277 - Args: cobra.ExactArgs(1), 278 - RunE: func(cmd *cobra.Command, args []string) error { 279 - svc := serviceFromCmd(cmd) 280 - contact, ok, err := svc.GetContactByPeerID(cmd.Context(), args[0]) 281 - if err != nil { 282 - return err 283 - } 284 - if !ok { 285 - return fmt.Errorf("contact not found: %s", args[0]) 286 - } 287 - fingerprint, _ := maep.FingerprintGrouped(contact.IdentityPubEd25519) 288 - view := map[string]any{ 289 - "peer_id": contact.PeerID, 290 - "node_uuid": contact.NodeUUID, 291 - "node_id": contact.NodeID, 292 - "display_name": contact.DisplayName, 293 - "identity_pub_ed25519": contact.IdentityPubEd25519, 294 - "fingerprint": fingerprint, 295 - "addresses": contact.Addresses, 296 - "trust_state": contact.TrustState, 297 - "min_supported_protocol": contact.MinSupportedProtocol, 298 - "max_supported_protocol": contact.MaxSupportedProtocol, 299 - "issued_at": contact.IssuedAt, 300 - "expires_at": contact.ExpiresAt, 301 - "updated_at": contact.UpdatedAt, 302 - } 303 - if outputJSON { 304 - return writeJSON(cmd.OutOrStdout(), view) 305 - } 306 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "peer_id: %s\nnode_uuid: %s\nnode_id: %s\ndisplay_name: %s\ntrust_state: %s\nfingerprint: %s\n", contact.PeerID, contact.NodeUUID, contact.NodeID, contact.DisplayName, contact.TrustState, fingerprint) 307 - for _, addr := range contact.Addresses { 308 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "address: %s\n", addr) 309 - } 310 - return nil 311 - }, 312 - } 313 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 314 - return cmd 315 - } 316 - 317 - func newContactsVerifyCmd() *cobra.Command { 318 - cmd := &cobra.Command{ 319 - Use: "verify <peer_id>", 320 - Short: "Mark contact as verified after out-of-band fingerprint check", 321 - Args: cobra.ExactArgs(1), 322 - RunE: func(cmd *cobra.Command, args []string) error { 323 - svc := serviceFromCmd(cmd) 324 - contact, err := svc.MarkContactVerified(cmd.Context(), args[0], time.Now().UTC()) 325 - if err != nil { 326 - return err 327 - } 328 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "peer_id: %s\ntrust_state: %s\n", contact.PeerID, contact.TrustState) 329 - return nil 330 - }, 331 - } 332 - return cmd 333 - } 334 - 335 - func newAuditCmd() *cobra.Command { 336 - cmd := &cobra.Command{ 337 - Use: "audit", 338 - Short: "Query MAEP trust-state and operation audit events", 339 - } 340 - cmd.AddCommand(newAuditListCmd()) 341 - return cmd 342 - } 343 - 344 - func newAuditListCmd() *cobra.Command { 345 - var peerID string 346 - var action string 347 - var limit int 348 - var outputJSON bool 349 - 350 - cmd := &cobra.Command{ 351 - Use: "list", 352 - Short: "List audit events", 353 - RunE: func(cmd *cobra.Command, args []string) error { 354 - svc := serviceFromCmd(cmd) 355 - events, err := svc.ListAuditEvents(cmd.Context(), peerID, action, limit) 356 - if err != nil { 357 - return err 358 - } 359 - if outputJSON { 360 - return writeJSON(cmd.OutOrStdout(), events) 361 - } 362 - if len(events) == 0 { 363 - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "no audit events") 364 - return nil 365 - } 366 - for _, event := range events { 367 - _, _ = fmt.Fprintf( 368 - cmd.OutOrStdout(), 369 - "%s\t%s\t%s\t%s\t%s\t%s\n", 370 - event.CreatedAt.UTC().Format(time.RFC3339), 371 - event.Action, 372 - event.PeerID, 373 - event.PreviousTrustState, 374 - event.NewTrustState, 375 - event.Reason, 376 - ) 377 - } 378 - return nil 379 - }, 380 - } 381 - cmd.Flags().StringVar(&peerID, "peer-id", "", "Filter by peer_id") 382 - cmd.Flags().StringVar(&action, "action", "", "Filter by action symbol") 383 - cmd.Flags().IntVar(&limit, "limit", 100, "Max number of records (<=0 means all)") 384 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 385 - return cmd 386 - } 387 - 388 - func newInboxCmd() *cobra.Command { 389 - cmd := &cobra.Command{ 390 - Use: "inbox", 391 - Short: "Query received agent.data.push messages", 392 - } 393 - cmd.AddCommand(newInboxListCmd()) 394 - return cmd 395 - } 396 - 397 - func newInboxListCmd() *cobra.Command { 398 - var fromPeerID string 399 - var topic string 400 - var limit int 401 - var outputJSON bool 402 - 403 - cmd := &cobra.Command{ 404 - Use: "list", 405 - Short: "List received messages from local inbox storage", 406 - RunE: func(cmd *cobra.Command, args []string) error { 407 - svc := serviceFromCmd(cmd) 408 - records, err := svc.ListInboxMessages(cmd.Context(), fromPeerID, topic, limit) 409 - if err != nil { 410 - return err 411 - } 412 - if outputJSON { 413 - return writeJSON(cmd.OutOrStdout(), records) 414 - } 415 - if len(records) == 0 { 416 - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "no inbox messages") 417 - return nil 418 - } 419 - writeInboxRecords(cmd.OutOrStdout(), records) 420 - return nil 421 - }, 422 - } 423 - cmd.Flags().StringVar(&fromPeerID, "from-peer-id", "", "Filter by sender peer_id") 424 - cmd.Flags().StringVar(&topic, "topic", "", "Filter by topic") 425 - cmd.Flags().IntVar(&limit, "limit", 50, "Max number of records (<=0 means all)") 426 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 427 - return cmd 428 - } 429 - 430 - func newOutboxCmd() *cobra.Command { 431 - cmd := &cobra.Command{ 432 - Use: "outbox", 433 - Short: "Query sent agent.data.push messages", 434 - } 435 - cmd.AddCommand(newOutboxListCmd()) 436 - return cmd 437 - } 438 - 439 - func newOutboxListCmd() *cobra.Command { 440 - var toPeerID string 441 - var topic string 442 - var limit int 443 - var outputJSON bool 444 - 445 - cmd := &cobra.Command{ 446 - Use: "list", 447 - Short: "List sent messages from local outbox storage", 448 - RunE: func(cmd *cobra.Command, args []string) error { 449 - svc := serviceFromCmd(cmd) 450 - records, err := svc.ListOutboxMessages(cmd.Context(), toPeerID, topic, limit) 451 - if err != nil { 452 - return err 453 - } 454 - if outputJSON { 455 - return writeJSON(cmd.OutOrStdout(), records) 456 - } 457 - if len(records) == 0 { 458 - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "no outbox messages") 459 - return nil 460 - } 461 - writeOutboxRecords(cmd.OutOrStdout(), records) 462 - return nil 463 - }, 464 - } 465 - cmd.Flags().StringVar(&toPeerID, "to-peer-id", "", "Filter by destination peer_id") 466 - cmd.Flags().StringVar(&topic, "topic", "", "Filter by topic") 467 - cmd.Flags().IntVar(&limit, "limit", 50, "Max number of records (<=0 means all)") 468 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 469 - return cmd 470 - } 471 - 472 - func newServeCmd() *cobra.Command { 473 - var listenAddrs []string 474 - var outputJSON bool 475 - var syncBusinessContacts bool 476 - cmd := &cobra.Command{ 477 - Use: "serve", 478 - Short: "Run MAEP libp2p node and handle incoming RPC streams", 479 - RunE: func(cmd *cobra.Command, args []string) error { 480 - runCtx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) 481 - defer stop() 482 - 483 - svc := serviceFromCmd(cmd) 484 - contactsStore := contacts.NewFileStore(statepaths.ContactsDir()) 485 - contactsSvc := contacts.NewService(contactsStore) 486 - logger := slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: slog.LevelInfo})) 487 - inprocBus, err := busruntime.StartInproc(busruntime.BootstrapOptions{ 488 - MaxInFlight: viper.GetInt("bus.max_inflight"), 489 - Logger: logger, 490 - Component: "maep", 491 - }) 492 - if err != nil { 493 - return err 494 - } 495 - defer inprocBus.Close() 496 - 497 - maepInboundAdapter, err := maepbus.NewInboundAdapter(maepbus.InboundAdapterOptions{ 498 - Bus: inprocBus, 499 - Store: contactsStore, 500 - }) 501 - if err != nil { 502 - return err 503 - } 504 - busHandler := func(ctx context.Context, msg busruntime.BusMessage) error { 505 - if msg.Direction != busruntime.DirectionInbound { 506 - return fmt.Errorf("unsupported direction: %s", msg.Direction) 507 - } 508 - if msg.Channel != busruntime.ChannelMAEP { 509 - return fmt.Errorf("unsupported inbound channel: %s", msg.Channel) 510 - } 511 - if syncBusinessContacts { 512 - if err := contactsSvc.ObserveInboundBusMessage(context.Background(), msg, svc, time.Now().UTC()); err != nil { 513 - logger.Warn("contacts_observe_maep_error", "idempotency_key", msg.IdempotencyKey, "error", err.Error()) 514 - return err 515 - } 516 - } 517 - event, err := maepbus.EventFromBusMessage(msg) 518 - if err != nil { 519 - return err 520 - } 521 - printDataPushEvent(cmd, event, outputJSON) 522 - return nil 523 - } 524 - for _, topic := range busruntime.AllTopics() { 525 - if err := inprocBus.Subscribe(topic, busHandler); err != nil { 526 - return err 527 - } 528 - } 529 - 530 - node, err := maep.NewNode(runCtx, svc, maep.NodeOptions{ 531 - ListenAddrs: listenAddrs, 532 - Logger: logger, 533 - OnDataPush: func(event maep.DataPushEvent) { 534 - accepted, publishErr := maepInboundAdapter.HandleDataPush(context.Background(), event) 535 - if publishErr != nil { 536 - logger.Warn("maep_bus_publish_error", "peer_id", event.FromPeerID, "topic", event.Topic, "bus_error_code", busruntime.ErrorCodeOf(publishErr), "error", publishErr.Error()) 537 - return 538 - } 539 - if !accepted { 540 - logger.Debug("maep_bus_deduped", "peer_id", event.FromPeerID, "topic", event.Topic, "idempotency_key", event.IdempotencyKey) 541 - } 542 - }, 543 - }) 544 - if err != nil { 545 - return err 546 - } 547 - defer node.Close() 548 - 549 - if outputJSON { 550 - _ = writeJSON(cmd.OutOrStdout(), map[string]any{ 551 - "status": "ready", 552 - "peer_id": node.PeerID(), 553 - "addresses": node.AddrStrings(), 554 - }) 555 - } else { 556 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "status: ready\npeer_id: %s\n", node.PeerID()) 557 - for _, addr := range node.AddrStrings() { 558 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "address: %s\n", addr) 559 - } 560 - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "waiting for incoming streams... (Ctrl+C to stop)") 561 - } 562 - 563 - <-runCtx.Done() 564 - return nil 565 - }, 566 - } 567 - cmd.Flags().StringArrayVar(&listenAddrs, "listen", nil, "Listen multiaddr (repeatable), default: /ip4/0.0.0.0/udp/0/quic-v1 and /ip4/0.0.0.0/tcp/0") 568 - cmd.Flags().BoolVar(&syncBusinessContacts, "sync-business-contacts", true, "Auto upsert inbound peers into contacts business store") 569 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print status/events as JSON") 570 - return cmd 571 - } 572 - 573 - func newHelloCmd() *cobra.Command { 574 - var addresses []string 575 - var outputJSON bool 576 - cmd := &cobra.Command{ 577 - Use: "hello <peer_id>", 578 - Short: "Run explicit hello negotiation with a contact", 579 - Args: cobra.ExactArgs(1), 580 - RunE: func(cmd *cobra.Command, args []string) error { 581 - node, err := newDialNode(cmd) 582 - if err != nil { 583 - return err 584 - } 585 - defer node.Close() 586 - result, err := node.DialHello(cmd.Context(), args[0], addresses) 587 - if err != nil { 588 - return err 589 - } 590 - if outputJSON { 591 - return writeJSON(cmd.OutOrStdout(), result) 592 - } 593 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "peer_id: %s\nremote_min: %d\nremote_max: %d\nnegotiated: %d\n", result.RemotePeerID, result.RemoteMinProtocol, result.RemoteMaxProtocol, result.NegotiatedProtocol) 594 - return nil 595 - }, 596 - } 597 - cmd.Flags().StringArrayVar(&addresses, "address", nil, "Override dial address (repeatable)") 598 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 599 - return cmd 600 - } 601 - 602 - func newPingCmd() *cobra.Command { 603 - var addresses []string 604 - var outputJSON bool 605 - cmd := &cobra.Command{ 606 - Use: "ping <peer_id>", 607 - Short: "Send agent.ping RPC to a contact", 608 - Args: cobra.ExactArgs(1), 609 - RunE: func(cmd *cobra.Command, args []string) error { 610 - node, err := newDialNode(cmd) 611 - if err != nil { 612 - return err 613 - } 614 - defer node.Close() 615 - result, err := node.Ping(cmd.Context(), args[0], addresses) 616 - if err != nil { 617 - return err 618 - } 619 - if outputJSON { 620 - return writeJSON(cmd.OutOrStdout(), result) 621 - } 622 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "ok: %v\nts: %v\n", result["ok"], result["ts"]) 623 - return nil 624 - }, 625 - } 626 - cmd.Flags().StringArrayVar(&addresses, "address", nil, "Override dial address (repeatable)") 627 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 628 - return cmd 629 - } 630 - 631 - func newCapabilitiesCmd() *cobra.Command { 632 - var addresses []string 633 - var outputJSON bool 634 - cmd := &cobra.Command{ 635 - Use: "capabilities <peer_id>", 636 - Short: "Fetch remote capabilities via agent.capabilities.get", 637 - Args: cobra.ExactArgs(1), 638 - RunE: func(cmd *cobra.Command, args []string) error { 639 - node, err := newDialNode(cmd) 640 - if err != nil { 641 - return err 642 - } 643 - defer node.Close() 644 - result, err := node.GetCapabilities(cmd.Context(), args[0], addresses) 645 - if err != nil { 646 - return err 647 - } 648 - if outputJSON { 649 - return writeJSON(cmd.OutOrStdout(), result) 650 - } 651 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "protocol_min: %v\nprotocol_max: %v\n", result["protocol_min"], result["protocol_max"]) 652 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "capabilities: %v\n", result["capabilities"]) 653 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "allowed_methods: %v\n", result["allowed_methods"]) 654 - return nil 655 - }, 656 - } 657 - cmd.Flags().StringArrayVar(&addresses, "address", nil, "Override dial address (repeatable)") 658 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 659 - return cmd 660 - } 661 - 662 - func newPushCmd() *cobra.Command { 663 - var addresses []string 664 - var topic string 665 - var text string 666 - var contentType string 667 - var idempotencyKey string 668 - var sessionID string 669 - var notify bool 670 - var outputJSON bool 671 - 672 - cmd := &cobra.Command{ 673 - Use: "push <peer_id>", 674 - Short: "Send agent.data.push to a contact", 675 - Args: cobra.ExactArgs(1), 676 - RunE: func(cmd *cobra.Command, args []string) error { 677 - text = strings.TrimSpace(text) 678 - if text == "" { 679 - return fmt.Errorf("--text is required") 680 - } 681 - topic = strings.TrimSpace(topic) 682 - if topic == "" { 683 - return fmt.Errorf("--topic is required") 684 - } 685 - contentType = strings.TrimSpace(contentType) 686 - if contentType == "" { 687 - contentType = "application/json" 688 - } 689 - if !strings.HasPrefix(strings.ToLower(contentType), "application/json") { 690 - return fmt.Errorf("--content-type must be application/json envelope") 691 - } 692 - resolvedSessionID := strings.TrimSpace(sessionID) 693 - if resolvedSessionID != "" { 694 - parsedSession, err := uuid.Parse(resolvedSessionID) 695 - if err != nil || parsedSession.Version() != uuid.Version(7) { 696 - return fmt.Errorf("--session-id must be uuid_v7") 697 - } 698 - } 699 - if maep.IsDialogueTopic(topic) && resolvedSessionID == "" { 700 - return fmt.Errorf("--session-id is required for dialogue topics") 701 - } 702 - 703 - messageID := uuid.NewString() 704 - payload := map[string]any{ 705 - "message_id": messageID, 706 - "text": text, 707 - "sent_at": time.Now().UTC().Format(time.RFC3339), 708 - } 709 - if resolvedSessionID != "" { 710 - payload["session_id"] = resolvedSessionID 711 - } 712 - payloadBytes, err := json.Marshal(payload) 713 - if err != nil { 714 - return err 715 - } 716 - 717 - idempotencyKey = strings.TrimSpace(idempotencyKey) 718 - if idempotencyKey == "" { 719 - idempotencyKey = idempotency.MessageEnvelopeKey(messageID) 720 - } 721 - 722 - req := maep.DataPushRequest{ 723 - Topic: topic, 724 - ContentType: contentType, 725 - PayloadBase64: base64.RawURLEncoding.EncodeToString(payloadBytes), 726 - IdempotencyKey: idempotencyKey, 727 - } 728 - 729 - node, err := newDialNode(cmd) 730 - if err != nil { 731 - return err 732 - } 733 - defer node.Close() 734 - 735 - result, err := node.PushData(cmd.Context(), args[0], addresses, req, notify) 736 - if err != nil { 737 - return err 738 - } 739 - if outputJSON { 740 - return writeJSON(cmd.OutOrStdout(), map[string]any{ 741 - "peer_id": args[0], 742 - "topic": req.Topic, 743 - "content_type": req.ContentType, 744 - "idempotency_key": req.IdempotencyKey, 745 - "session_id": resolvedSessionID, 746 - "notification": notify, 747 - "result": result, 748 - }) 749 - } 750 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "peer_id: %s\ntopic: %s\ncontent_type: %s\nsession_id: %s\nidempotency_key: %s\nnotification: %v\naccepted: %v\ndeduped: %v\n", args[0], req.Topic, req.ContentType, resolvedSessionID, req.IdempotencyKey, notify, result.Accepted, result.Deduped) 751 - return nil 752 - }, 753 - } 754 - 755 - cmd.Flags().StringArrayVar(&addresses, "address", nil, "Override dial address (repeatable)") 756 - cmd.Flags().StringVar(&topic, "topic", "chat.message", "Data topic") 757 - cmd.Flags().StringVar(&text, "text", "", "Text payload") 758 - cmd.Flags().StringVar(&contentType, "content-type", "application/json", "Content type (must be application/json envelope)") 759 - cmd.Flags().StringVar(&idempotencyKey, "idempotency-key", "", "Idempotency key (default: derived from message_id)") 760 - cmd.Flags().StringVar(&sessionID, "session-id", "", "Session id for JSON payload") 761 - cmd.Flags().BoolVar(&notify, "notify", false, "Send as JSON-RPC notification (no response expected)") 762 - cmd.Flags().BoolVar(&outputJSON, "json", false, "Print as JSON") 763 - return cmd 764 - } 765 - 766 - func newDialNode(cmd *cobra.Command) (*maep.Node, error) { 767 - svc := serviceFromCmd(cmd) 768 - logger := slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: slog.LevelInfo})) 769 - return maep.NewNode(cmd.Context(), svc, maep.NodeOptions{DialOnly: true, Logger: logger}) 770 - } 771 - 772 - func printDataPushEvent(cmd *cobra.Command, event maep.DataPushEvent, outputJSON bool) { 773 - if outputJSON { 774 - payloadText := "" 775 - if strings.HasPrefix(strings.ToLower(event.ContentType), "text/") || strings.EqualFold(event.ContentType, "application/json") { 776 - payloadText = string(event.PayloadBytes) 777 - } 778 - _ = writeJSON(cmd.OutOrStdout(), map[string]any{ 779 - "event": "agent.data.push", 780 - "from_peer_id": event.FromPeerID, 781 - "topic": event.Topic, 782 - "content_type": event.ContentType, 783 - "idempotency_key": event.IdempotencyKey, 784 - "session_id": event.SessionID, 785 - "reply_to": event.ReplyTo, 786 - "deduped": event.Deduped, 787 - "received_at": event.ReceivedAt, 788 - "payload_text": payloadText, 789 - "payload_base64": event.PayloadBase64, 790 - }) 791 - return 792 - } 793 - 794 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "incoming: topic=%s from=%s session_id=%s deduped=%v idempotency_key=%s\n", event.Topic, event.FromPeerID, event.SessionID, event.Deduped, event.IdempotencyKey) 795 - if strings.HasPrefix(strings.ToLower(event.ContentType), "application/json") { 796 - var obj any 797 - if err := json.Unmarshal(event.PayloadBytes, &obj); err == nil { 798 - pretty, _ := json.MarshalIndent(obj, "", " ") 799 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "payload(json): %s\n", string(pretty)) 800 - return 801 - } 802 - } 803 - if strings.HasPrefix(strings.ToLower(event.ContentType), "text/") { 804 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "payload(text): %s\n", string(event.PayloadBytes)) 805 - return 806 - } 807 - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "payload(bytes): %d\n", len(event.PayloadBytes)) 808 - } 809 - 810 - func summarizePayload(contentType string, payloadBase64 string) string { 811 - data, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(payloadBase64)) 812 - if err != nil { 813 - return "<invalid-base64>" 814 - } 815 - lowerType := strings.ToLower(strings.TrimSpace(contentType)) 816 - if strings.HasPrefix(lowerType, "application/json") { 817 - var obj any 818 - if err := json.Unmarshal(data, &obj); err != nil { 819 - return "<invalid-json>" 820 - } 821 - if isMAEPEnvelopeJSON(obj) { 822 - text, err := json.MarshalIndent(obj, "", " ") 823 - if err != nil { 824 - return "<json-encode-error>" 825 - } 826 - return string(text) 827 - } 828 - text, err := json.Marshal(obj) 829 - if err != nil { 830 - return "<json-encode-error>" 831 - } 832 - return string(text) 833 - } 834 - if strings.HasPrefix(lowerType, "text/") { 835 - return string(data) 836 - } 837 - return fmt.Sprintf("<%d bytes>", len(data)) 838 - } 839 - 840 - func writeInboxRecords(w io.Writer, records []maep.InboxMessage) { 841 - for i, record := range records { 842 - payloadSummary := summarizePayload(record.ContentType, record.PayloadBase64) 843 - _, _ = fmt.Fprintf(w, "[%d]\n", i+1) 844 - _, _ = fmt.Fprintf(w, "received_at: %s\n", record.ReceivedAt.UTC().Format(time.RFC3339)) 845 - _, _ = fmt.Fprintf(w, "from_peer_id: %s\n", record.FromPeerID) 846 - _, _ = fmt.Fprintf(w, "topic: %s\n", record.Topic) 847 - _, _ = fmt.Fprintf(w, "session_id: %s\n", record.SessionID) 848 - if strings.TrimSpace(record.ReplyTo) != "" { 849 - _, _ = fmt.Fprintf(w, "reply_to: %s\n", record.ReplyTo) 850 - } 851 - _, _ = fmt.Fprintf(w, "idempotency_key: %s\n", record.IdempotencyKey) 852 - _, _ = fmt.Fprintf(w, "content_type: %s\n", record.ContentType) 853 - _, _ = fmt.Fprintln(w, "payload:") 854 - _, _ = fmt.Fprintln(w, indentBlock(payloadSummary, " ")) 855 - if i < len(records)-1 { 856 - _, _ = fmt.Fprintln(w, "") 857 - } 858 - } 859 - } 860 - 861 - func writeOutboxRecords(w io.Writer, records []maep.OutboxMessage) { 862 - for i, record := range records { 863 - payloadSummary := summarizePayload(record.ContentType, record.PayloadBase64) 864 - _, _ = fmt.Fprintf(w, "[%d]\n", i+1) 865 - _, _ = fmt.Fprintf(w, "sent_at: %s\n", record.SentAt.UTC().Format(time.RFC3339)) 866 - _, _ = fmt.Fprintf(w, "to_peer_id: %s\n", record.ToPeerID) 867 - _, _ = fmt.Fprintf(w, "topic: %s\n", record.Topic) 868 - _, _ = fmt.Fprintf(w, "session_id: %s\n", record.SessionID) 869 - if strings.TrimSpace(record.ReplyTo) != "" { 870 - _, _ = fmt.Fprintf(w, "reply_to: %s\n", record.ReplyTo) 871 - } 872 - _, _ = fmt.Fprintf(w, "idempotency_key: %s\n", record.IdempotencyKey) 873 - _, _ = fmt.Fprintf(w, "content_type: %s\n", record.ContentType) 874 - _, _ = fmt.Fprintln(w, "payload:") 875 - _, _ = fmt.Fprintln(w, indentBlock(payloadSummary, " ")) 876 - if i < len(records)-1 { 877 - _, _ = fmt.Fprintln(w, "") 878 - } 879 - } 880 - } 881 - 882 - func indentBlock(text string, prefix string) string { 883 - prefix = strings.TrimRight(prefix, "\n") 884 - if prefix == "" { 885 - prefix = " " 886 - } 887 - if strings.TrimSpace(text) == "" { 888 - return prefix + "(empty)" 889 - } 890 - lines := strings.Split(text, "\n") 891 - for i := range lines { 892 - lines[i] = prefix + lines[i] 893 - } 894 - return strings.Join(lines, "\n") 895 - } 896 - 897 - func isMAEPEnvelopeJSON(v any) bool { 898 - obj, ok := v.(map[string]any) 899 - if !ok { 900 - return false 901 - } 902 - if _, ok := obj["message_id"].(string); !ok { 903 - return false 904 - } 905 - if _, ok := obj["text"].(string); !ok { 906 - return false 907 - } 908 - if _, ok := obj["sent_at"].(string); !ok { 909 - return false 910 - } 911 - return true 912 - } 913 - 914 - func resolveCardExportAddresses(ctx context.Context, svc *maep.Service, explicit []string, configuredListenAddrs []string) ([]string, error) { 915 - return resolveCardExportAddressesWithPrompt(ctx, svc, explicit, configuredListenAddrs, nil, nil, false) 916 - } 917 - 918 - func resolveCardExportAddressesForCommand( 919 - ctx context.Context, 920 - svc *maep.Service, 921 - explicit []string, 922 - configuredListenAddrs []string, 923 - in io.Reader, 924 - out io.Writer, 925 - ) ([]string, error) { 926 - return resolveCardExportAddressesWithPrompt(ctx, svc, explicit, configuredListenAddrs, in, out, isInteractiveTerminal(in, out)) 927 - } 928 - 929 - func resolveCardExportAddressesWithPrompt( 930 - ctx context.Context, 931 - svc *maep.Service, 932 - explicit []string, 933 - configuredListenAddrs []string, 934 - in io.Reader, 935 - out io.Writer, 936 - interactive bool, 937 - ) ([]string, error) { 938 - normalizedExplicit := normalizeAddressList(explicit) 939 - if len(normalizedExplicit) > 0 { 940 - return normalizedExplicit, nil 941 - } 942 - normalizedConfigured := normalizeAddressList(configuredListenAddrs) 943 - if len(normalizedConfigured) == 0 { 944 - return nil, fmt.Errorf("at least one --address is required (or set maep.listen_addrs in config)") 945 - } 946 - 947 - identity, ok, err := svc.GetIdentity(ctx) 948 - if err != nil { 949 - return nil, err 950 - } 951 - if !ok { 952 - return nil, fmt.Errorf("identity not found; run `mistermorph maep init`") 953 - } 954 - resolved, err := appendPeerIDToAddresses(normalizedConfigured, identity.PeerID) 955 - if err != nil { 956 - return nil, err 957 - } 958 - resolved = expandConfiguredDialAddresses(resolved) 959 - return selectCardExportDialAddresses(resolved, in, out, interactive) 960 - } 961 - 962 - func appendPeerIDToAddresses(addresses []string, peerID string) ([]string, error) { 963 - peerID = strings.TrimSpace(peerID) 964 - if peerID == "" { 965 - return nil, fmt.Errorf("empty local peer_id") 966 - } 967 - peerComponent, err := ma.NewMultiaddr("/p2p/" + peerID) 968 - if err != nil { 969 - return nil, fmt.Errorf("build /p2p component: %w", err) 970 - } 971 - 972 - out := make([]string, 0, len(addresses)) 973 - seen := map[string]bool{} 974 - for _, raw := range addresses { 975 - addr := strings.TrimSpace(raw) 976 - if addr == "" { 977 - continue 978 - } 979 - maddr, err := ma.NewMultiaddr(addr) 980 - if err != nil { 981 - return nil, fmt.Errorf("invalid configured address %q: %w", addr, err) 982 - } 983 - if _, last := ma.SplitLast(maddr); last != nil && last.Protocol().Code == ma.P_P2P { 984 - if strings.TrimSpace(last.Value()) != peerID { 985 - return nil, fmt.Errorf("configured address %q has mismatched /p2p/%s (local peer_id=%s)", addr, last.Value(), peerID) 986 - } 987 - canonical := maddr.String() 988 - if !seen[canonical] { 989 - seen[canonical] = true 990 - out = append(out, canonical) 991 - } 992 - continue 993 - } 994 - 995 - withPeer := maddr.Encapsulate(peerComponent).String() 996 - if seen[withPeer] { 997 - continue 998 - } 999 - seen[withPeer] = true 1000 - out = append(out, withPeer) 1001 - } 1002 - return out, nil 1003 - } 1004 - 1005 - func expandConfiguredDialAddresses(addresses []string) []string { 1006 - localIPs, err := discoverLocalInterfaceIPs() 1007 - if err != nil || len(localIPs) == 0 { 1008 - return normalizeAddressList(addresses) 1009 - } 1010 - return expandConfiguredDialAddressesWithIPs(addresses, localIPs) 1011 - } 1012 - 1013 - func expandConfiguredDialAddressesWithIPs(addresses []string, localIPs []net.IP) []string { 1014 - out := make([]string, 0, len(addresses)) 1015 - seen := map[string]bool{} 1016 - add := func(addr string) { 1017 - addr = strings.TrimSpace(addr) 1018 - if addr == "" || seen[addr] { 1019 - return 1020 - } 1021 - seen[addr] = true 1022 - out = append(out, addr) 1023 - } 1024 - 1025 - for _, raw := range normalizeAddressList(addresses) { 1026 - add(raw) 1027 - 1028 - maddr, err := ma.NewMultiaddr(raw) 1029 - if err != nil { 1030 - continue 1031 - } 1032 - if value, err := maddr.ValueForProtocol(ma.P_IP4); err == nil { 1033 - if ip := net.ParseIP(strings.TrimSpace(value)); ip != nil && ip.IsUnspecified() { 1034 - for _, local := range localIPs { 1035 - v4 := local.To4() 1036 - if v4 == nil { 1037 - continue 1038 - } 1039 - replaced, err := replaceMultiaddrIPComponent(maddr, ma.P_IP4, v4.String()) 1040 - if err != nil { 1041 - continue 1042 - } 1043 - add(replaced.String()) 1044 - } 1045 - } 1046 - } 1047 - if value, err := maddr.ValueForProtocol(ma.P_IP6); err == nil { 1048 - if ip := net.ParseIP(strings.TrimSpace(value)); ip != nil && ip.IsUnspecified() { 1049 - for _, local := range localIPs { 1050 - if local.To4() != nil || local.To16() == nil { 1051 - continue 1052 - } 1053 - replaced, err := replaceMultiaddrIPComponent(maddr, ma.P_IP6, local.String()) 1054 - if err != nil { 1055 - continue 1056 - } 1057 - add(replaced.String()) 1058 - } 1059 - } 1060 - } 1061 - } 1062 - return out 1063 - } 1064 - 1065 - func replaceMultiaddrIPComponent(addr ma.Multiaddr, protoCode int, value string) (ma.Multiaddr, error) { 1066 - if addr == nil { 1067 - return nil, fmt.Errorf("nil multiaddr") 1068 - } 1069 - value = strings.TrimSpace(value) 1070 - if value == "" { 1071 - return addr, nil 1072 - } 1073 - raw := addr.String() 1074 - updated := raw 1075 - switch protoCode { 1076 - case ma.P_IP4: 1077 - if strings.Contains(raw, "/ip4/0.0.0.0/") { 1078 - updated = strings.Replace(raw, "/ip4/0.0.0.0/", "/ip4/"+value+"/", 1) 1079 - } else if strings.HasSuffix(raw, "/ip4/0.0.0.0") { 1080 - updated = strings.TrimSuffix(raw, "/ip4/0.0.0.0") + "/ip4/" + value 1081 - } 1082 - case ma.P_IP6: 1083 - if strings.Contains(raw, "/ip6/::/") { 1084 - updated = strings.Replace(raw, "/ip6/::/", "/ip6/"+value+"/", 1) 1085 - } else if strings.HasSuffix(raw, "/ip6/::") { 1086 - updated = strings.TrimSuffix(raw, "/ip6/::") + "/ip6/" + value 1087 - } 1088 - default: 1089 - return addr, nil 1090 - } 1091 - if updated == raw { 1092 - return addr, nil 1093 - } 1094 - rebuilt, err := ma.NewMultiaddr(updated) 1095 - if err != nil { 1096 - return nil, err 1097 - } 1098 - return rebuilt, nil 1099 - } 1100 - 1101 - func discoverLocalInterfaceIPs() ([]net.IP, error) { 1102 - ifaces, err := net.Interfaces() 1103 - if err != nil { 1104 - return nil, err 1105 - } 1106 - out := make([]net.IP, 0, 8) 1107 - seen := map[string]bool{} 1108 - for _, iface := range ifaces { 1109 - if iface.Flags&net.FlagUp == 0 { 1110 - continue 1111 - } 1112 - addrs, err := iface.Addrs() 1113 - if err != nil { 1114 - continue 1115 - } 1116 - for _, addr := range addrs { 1117 - ip := interfaceAddrIP(addr) 1118 - if ip == nil || ip.IsUnspecified() { 1119 - continue 1120 - } 1121 - key := ip.String() 1122 - if seen[key] { 1123 - continue 1124 - } 1125 - seen[key] = true 1126 - out = append(out, ip) 1127 - } 1128 - } 1129 - sort.SliceStable(out, func(i, j int) bool { 1130 - if out[i].IsLoopback() != out[j].IsLoopback() { 1131 - return !out[i].IsLoopback() 1132 - } 1133 - isV4i := out[i].To4() != nil 1134 - isV4j := out[j].To4() != nil 1135 - if isV4i != isV4j { 1136 - return isV4i 1137 - } 1138 - return out[i].String() < out[j].String() 1139 - }) 1140 - return out, nil 1141 - } 1142 - 1143 - func interfaceAddrIP(addr net.Addr) net.IP { 1144 - switch v := addr.(type) { 1145 - case *net.IPNet: 1146 - if v == nil || v.IP == nil { 1147 - return nil 1148 - } 1149 - return v.IP 1150 - case *net.IPAddr: 1151 - if v == nil || v.IP == nil { 1152 - return nil 1153 - } 1154 - return v.IP 1155 - default: 1156 - return nil 1157 - } 1158 - } 1159 - 1160 - func selectCardExportDialAddresses(addresses []string, in io.Reader, out io.Writer, interactive bool) ([]string, error) { 1161 - valid, invalid := classifyCardExportDialAddresses(addresses) 1162 - if len(valid) == 0 { 1163 - if len(invalid) == 0 { 1164 - return nil, fmt.Errorf("no dialable addresses available (provide --address explicitly)") 1165 - } 1166 - reasons := make([]string, 0, len(invalid)) 1167 - for _, item := range invalid { 1168 - reasons = append(reasons, fmt.Sprintf("%s (%s)", item.Address, item.Reason)) 1169 - } 1170 - return nil, fmt.Errorf("no dialable addresses available from maep.listen_addrs: %s", strings.Join(reasons, "; ")) 1171 - } 1172 - if !interactive { 1173 - if len(valid) == 1 { 1174 - return []string{valid[0]}, nil 1175 - } 1176 - return nil, fmt.Errorf("multiple dialable addresses found; pass --address explicitly or run in an interactive terminal") 1177 - } 1178 - 1179 - if out == nil { 1180 - out = os.Stderr 1181 - } 1182 - if in == nil { 1183 - in = os.Stdin 1184 - } 1185 - _, _ = fmt.Fprintln(out, "No --address provided. Select one dialable address for contact card:") 1186 - for i, addr := range valid { 1187 - _, _ = fmt.Fprintf(out, " %d) %s\n", i+1, addr) 1188 - } 1189 - if len(invalid) > 0 { 1190 - _, _ = fmt.Fprintln(out, "Ignored non-dialable addresses:") 1191 - for _, item := range invalid { 1192 - _, _ = fmt.Fprintf(out, " - %s (%s)\n", item.Address, item.Reason) 1193 - } 1194 - } 1195 - 1196 - reader := bufio.NewReader(in) 1197 - for { 1198 - _, _ = fmt.Fprintf(out, "Select address [1-%d] (default 1): ", len(valid)) 1199 - line, err := reader.ReadString('\n') 1200 - if err != nil && err != io.EOF { 1201 - return nil, fmt.Errorf("read selection: %w", err) 1202 - } 1203 - choice := strings.TrimSpace(line) 1204 - if choice == "" { 1205 - return []string{valid[0]}, nil 1206 - } 1207 - if strings.EqualFold(choice, "q") || strings.EqualFold(choice, "quit") { 1208 - return nil, fmt.Errorf("card export cancelled") 1209 - } 1210 - index, parseErr := strconv.Atoi(choice) 1211 - if parseErr == nil && index >= 1 && index <= len(valid) { 1212 - return []string{valid[index-1]}, nil 1213 - } 1214 - _, _ = fmt.Fprintf(out, "Invalid selection: %q\n", choice) 1215 - if err == io.EOF { 1216 - return nil, fmt.Errorf("invalid selection: %q", choice) 1217 - } 1218 - } 1219 - } 1220 - 1221 - type invalidDialAddress struct { 1222 - Address string 1223 - Reason string 1224 - } 1225 - 1226 - func classifyCardExportDialAddresses(addresses []string) ([]string, []invalidDialAddress) { 1227 - valid := make([]string, 0, len(addresses)) 1228 - invalid := make([]invalidDialAddress, 0, len(addresses)) 1229 - for _, raw := range addresses { 1230 - address := strings.TrimSpace(raw) 1231 - if address == "" { 1232 - continue 1233 - } 1234 - reason := dialAddressInvalidReason(address) 1235 - if reason != "" { 1236 - invalid = append(invalid, invalidDialAddress{Address: address, Reason: reason}) 1237 - continue 1238 - } 1239 - valid = append(valid, address) 1240 - } 1241 - return valid, invalid 1242 - } 1243 - 1244 - func dialAddressInvalidReason(address string) string { 1245 - maddr, err := ma.NewMultiaddr(address) 1246 - if err != nil { 1247 - return "invalid multiaddr" 1248 - } 1249 - if value, err := maddr.ValueForProtocol(ma.P_IP4); err == nil { 1250 - if ip := net.ParseIP(strings.TrimSpace(value)); ip != nil && ip.IsUnspecified() { 1251 - return "ip4 unspecified" 1252 - } 1253 - } 1254 - if value, err := maddr.ValueForProtocol(ma.P_IP6); err == nil { 1255 - if ip := net.ParseIP(strings.TrimSpace(value)); ip != nil && ip.IsUnspecified() { 1256 - return "ip6 unspecified" 1257 - } 1258 - } 1259 - if value, err := maddr.ValueForProtocol(ma.P_TCP); err == nil && strings.TrimSpace(value) == "0" { 1260 - return "tcp port 0" 1261 - } 1262 - if value, err := maddr.ValueForProtocol(ma.P_UDP); err == nil && strings.TrimSpace(value) == "0" { 1263 - return "udp port 0" 1264 - } 1265 - return "" 1266 - } 1267 - 1268 - func isInteractiveTerminal(in io.Reader, out io.Writer) bool { 1269 - inFile, ok := in.(*os.File) 1270 - if !ok { 1271 - return false 1272 - } 1273 - outFile, ok := out.(*os.File) 1274 - if !ok { 1275 - return false 1276 - } 1277 - inInfo, err := inFile.Stat() 1278 - if err != nil { 1279 - return false 1280 - } 1281 - outInfo, err := outFile.Stat() 1282 - if err != nil { 1283 - return false 1284 - } 1285 - return (inInfo.Mode()&os.ModeCharDevice) != 0 && (outInfo.Mode()&os.ModeCharDevice) != 0 1286 - } 1287 - 1288 - func normalizeAddressList(values []string) []string { 1289 - if len(values) == 0 { 1290 - return nil 1291 - } 1292 - out := make([]string, 0, len(values)) 1293 - seen := map[string]bool{} 1294 - for _, raw := range values { 1295 - value := strings.TrimSpace(raw) 1296 - if value == "" || seen[value] { 1297 - continue 1298 - } 1299 - seen[value] = true 1300 - out = append(out, value) 1301 - } 1302 - return out 1303 - } 1304 - 1305 - func serviceFromCmd(cmd *cobra.Command) *maep.Service { 1306 - dir, _ := cmd.Flags().GetString("dir") 1307 - dir = strings.TrimSpace(dir) 1308 - if dir == "" { 1309 - dir = statepaths.MAEPDir() 1310 - } else { 1311 - dir = pathutil.ExpandHomePath(dir) 1312 - } 1313 - store := maep.NewFileStore(dir) 1314 - return maep.NewService(store) 1315 - } 1316 - 1317 - func readInputFile(path string) ([]byte, error) { 1318 - if path == "-" { 1319 - return io.ReadAll(os.Stdin) 1320 - } 1321 - return os.ReadFile(path) 1322 - } 1323 - 1324 - func writeJSON(w io.Writer, v any) error { 1325 - enc := json.NewEncoder(w) 1326 - enc.SetIndent("", " ") 1327 - return enc.Encode(v) 1328 - }
-3
cmd/mistermorph/registry.go
··· 48 48 TODOPathWIP string 49 49 TODOPathDone string 50 50 ContactsDir string 51 - MAEPDir string 52 51 TelegramBotToken string 53 52 TelegramBaseURL string 54 53 SlackBotToken string ··· 125 124 TODOPathWIP: pathutil.ResolveStateFile(fileStateDir, statepaths.TODOWIPFilename), 126 125 TODOPathDone: pathutil.ResolveStateFile(fileStateDir, statepaths.TODODONEFilename), 127 126 ContactsDir: pathutil.ResolveStateChildDir(fileStateDir, strings.TrimSpace(viper.GetString("contacts.dir_name")), "contacts"), 128 - MAEPDir: pathutil.ResolveStateChildDir(fileStateDir, strings.TrimSpace(viper.GetString("maep.dir_name")), "maep"), 129 127 TelegramBotToken: strings.TrimSpace(viper.GetString("telegram.bot_token")), 130 128 TelegramBaseURL: "https://api.telegram.org", 131 129 SlackBotToken: strings.TrimSpace(viper.GetString("slack.bot_token")), ··· 253 251 r.Register(builtin.NewContactsSendTool(builtin.ContactsSendToolOptions{ 254 252 Enabled: true, 255 253 ContactsDir: cfg.ContactsDir, 256 - MAEPDir: cfg.MAEPDir, 257 254 TelegramBotToken: strings.TrimSpace(cfg.TelegramBotToken), 258 255 TelegramBaseURL: strings.TrimSpace(cfg.TelegramBaseURL), 259 256 SlackBotToken: strings.TrimSpace(cfg.SlackBotToken),
-2
cmd/mistermorph/root.go
··· 13 13 "github.com/quailyquaily/mistermorph/cmd/mistermorph/consolecmd" 14 14 "github.com/quailyquaily/mistermorph/cmd/mistermorph/contactscmd" 15 15 "github.com/quailyquaily/mistermorph/cmd/mistermorph/daemoncmd" 16 - "github.com/quailyquaily/mistermorph/cmd/mistermorph/maepcmd" 17 16 "github.com/quailyquaily/mistermorph/cmd/mistermorph/runcmd" 18 17 "github.com/quailyquaily/mistermorph/cmd/mistermorph/skillscmd" 19 18 "github.com/quailyquaily/mistermorph/cmd/mistermorph/slackcmd" ··· 172 171 })) 173 172 cmd.AddCommand(newToolsCmd(registryResolver.Registry)) 174 173 cmd.AddCommand(skillscmd.New()) 175 - cmd.AddCommand(maepcmd.New()) 176 174 cmd.AddCommand(contactscmd.New()) 177 175 cmd.AddCommand(consolecmd.New()) 178 176 cmd.AddCommand(newInstallCmd())
-4
cmd/mistermorph/telegramcmd/command.go
··· 39 39 GroupTriggerMode: strings.TrimSpace(configutil.FlagOrViperString(cmd, "telegram-group-trigger-mode", "telegram.group_trigger_mode")), 40 40 AddressingConfidenceThreshold: configutil.FlagOrViperFloat64(cmd, "telegram-addressing-confidence-threshold", "telegram.addressing_confidence_threshold"), 41 41 AddressingInterjectThreshold: configutil.FlagOrViperFloat64(cmd, "telegram-addressing-interject-threshold", "telegram.addressing_interject_threshold"), 42 - WithMAEP: configutil.FlagOrViperBool(cmd, "with-maep", "telegram.with_maep"), 43 - MAEPListenAddrs: configutil.FlagOrViperStringArray(cmd, "maep-listen", "maep.listen_addrs"), 44 42 PollTimeout: configutil.FlagOrViperDuration(cmd, "telegram-poll-timeout", "telegram.poll_timeout"), 45 43 TaskTimeout: configutil.FlagOrViperDuration(cmd, "telegram-task-timeout", "telegram.task_timeout"), 46 44 MaxConcurrency: configutil.FlagOrViperInt(cmd, "telegram-max-concurrency", "telegram.max_concurrency"), ··· 59 57 cmd.Flags().String("telegram-group-trigger-mode", "smart", "Group trigger mode: strict|smart|talkative.") 60 58 cmd.Flags().Float64("telegram-addressing-confidence-threshold", 0.6, "Minimum confidence (0-1) required to accept an addressing LLM decision.") 61 59 cmd.Flags().Float64("telegram-addressing-interject-threshold", 0.6, "Minimum interject (0-1) allowed to accept an addressing LLM decision.") 62 - cmd.Flags().Bool("with-maep", false, "Start MAEP listener together with telegram mode.") 63 - cmd.Flags().StringArray("maep-listen", nil, "MAEP listen multiaddr for --with-maep (repeatable). Defaults to maep.listen_addrs or MAEP defaults.") 64 60 cmd.Flags().Duration("telegram-poll-timeout", 30*time.Second, "Long polling timeout for getUpdates.") 65 61 cmd.Flags().Duration("telegram-task-timeout", 0, "Per-message agent timeout (0 uses --timeout).") 66 62 cmd.Flags().Int("telegram-max-concurrency", 3, "Max number of chats processed concurrently.")
+1 -152
contacts/bus_observe.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "regexp" 7 - "sort" 8 6 "strconv" 9 7 "strings" 10 8 "time" 11 9 12 10 busruntime "github.com/quailyquaily/mistermorph/internal/bus" 13 - "github.com/quailyquaily/mistermorph/maep" 14 11 ) 15 12 16 - var maepMentionTokenPattern = regexp.MustCompile(`(?i)\bmaep:[a-z0-9]+\b`) 17 - 18 - type MAEPPeerLookup interface { 19 - GetContactByPeerID(ctx context.Context, peerID string) (maep.Contact, bool, error) 20 - } 21 - 22 13 type observedContactCandidate struct { 23 14 PrimaryContactID string 24 15 AlternateContactIDs []string ··· 33 24 SlackUserID string 34 25 SlackDMChannelID string 35 26 SlackChannelIDs []string 36 - MAEPNodeID string 37 - MAEPDialAddress string 38 27 } 39 28 40 29 // ObserveInboundBusMessage inspects inbound bus messages and updates contacts. 41 30 // It is best-effort for object extraction and follows merge rules for bus-driven contact updates. 42 - func (s *Service) ObserveInboundBusMessage(ctx context.Context, msg busruntime.BusMessage, maepLookup MAEPPeerLookup, now time.Time) error { 31 + func (s *Service) ObserveInboundBusMessage(ctx context.Context, msg busruntime.BusMessage, now time.Time) error { 43 32 if s == nil || !s.ready() { 44 33 return fmt.Errorf("nil contacts service") 45 34 } ··· 56 45 return s.observeTelegramInboundBusMessage(ctx, msg, now) 57 46 case busruntime.ChannelSlack: 58 47 return s.observeSlackInboundBusMessage(ctx, msg, now) 59 - case busruntime.ChannelMAEP: 60 - return s.observeMAEPInboundBusMessage(ctx, msg, maepLookup, now) 61 48 default: 62 49 return nil 63 50 } ··· 113 100 return s.applyObservedCandidates(ctx, candidates, now) 114 101 } 115 102 116 - func (s *Service) observeMAEPInboundBusMessage(ctx context.Context, msg busruntime.BusMessage, maepLookup MAEPPeerLookup, now time.Time) error { 117 - peerID := resolveMAEPPeerIDFromBusMessage(msg) 118 - if peerID == "" { 119 - return nil 120 - } 121 - 122 - candidates := make([]observedContactCandidate, 0, 4) 123 - senderCandidate, err := makeMAEPCandidate(ctx, peerID, maepLookup) 124 - if err != nil { 125 - return err 126 - } 127 - if senderCandidate.PrimaryContactID != "" { 128 - candidates = append(candidates, senderCandidate) 129 - } 130 - 131 - env, envErr := msg.Envelope() 132 - if envErr == nil { 133 - for _, mentionedPeerID := range extractMAEPMentionPeerIDs(env.Text) { 134 - if strings.EqualFold(mentionedPeerID, peerID) { 135 - continue 136 - } 137 - mentionedCandidate, mentionErr := makeMAEPCandidate(ctx, mentionedPeerID, maepLookup) 138 - if mentionErr != nil { 139 - return mentionErr 140 - } 141 - if mentionedCandidate.PrimaryContactID == "" { 142 - continue 143 - } 144 - candidates = append(candidates, mentionedCandidate) 145 - } 146 - } 147 - 148 - return s.applyObservedCandidates(ctx, candidates, now) 149 - } 150 - 151 103 func (s *Service) observeSlackInboundBusMessage(ctx context.Context, msg busruntime.BusMessage, now time.Time) error { 152 104 teamID, channelID, err := slackConversationPartsFromKey(msg.ConversationKey) 153 105 if err != nil { ··· 206 158 return s.applyObservedCandidates(ctx, candidates, now) 207 159 } 208 160 209 - func makeMAEPCandidate(ctx context.Context, peerID string, maepLookup MAEPPeerLookup) (observedContactCandidate, error) { 210 - peerID = strings.TrimSpace(peerID) 211 - if peerID == "" { 212 - return observedContactCandidate{}, nil 213 - } 214 - 215 - nodeID := "" 216 - dialAddress := "" 217 - nickname := "" 218 - if maepLookup != nil { 219 - maepContact, found, err := maepLookup.GetContactByPeerID(ctx, peerID) 220 - if err != nil { 221 - return observedContactCandidate{}, err 222 - } 223 - if found { 224 - nodeID = strings.TrimSpace(maepContact.NodeID) 225 - if len(maepContact.Addresses) > 0 { 226 - dialAddress = strings.TrimSpace(maepContact.Addresses[0]) 227 - } 228 - nickname = strings.TrimSpace(maepContact.DisplayName) 229 - } 230 - } 231 - 232 - primaryID := chooseMAEPBusinessContactID(nodeID, peerID) 233 - candidate := observedContactCandidate{ 234 - PrimaryContactID: primaryID, 235 - Kind: KindAgent, 236 - Channel: ChannelMAEP, 237 - Nickname: nickname, 238 - MAEPNodeID: strings.TrimSpace(nodeID), 239 - MAEPDialAddress: dialAddress, 240 - } 241 - peerContactID := "maep:" + peerID 242 - if !strings.EqualFold(strings.TrimSpace(primaryID), peerContactID) { 243 - candidate.AlternateContactIDs = append(candidate.AlternateContactIDs, peerContactID) 244 - } 245 - return candidate, nil 246 - } 247 - 248 - func chooseMAEPBusinessContactID(nodeID string, peerID string) string { 249 - nodeID = strings.TrimSpace(nodeID) 250 - if nodeID != "" { 251 - normalizedNodeID, _ := splitMAEPNodeID(nodeID) 252 - return normalizedNodeID 253 - } 254 - peerID = strings.TrimSpace(peerID) 255 - if peerID == "" { 256 - return "" 257 - } 258 - return "maep:" + peerID 259 - } 260 - 261 - func resolveMAEPPeerIDFromBusMessage(msg busruntime.BusMessage) string { 262 - peerID := strings.TrimSpace(msg.ParticipantKey) 263 - if peerID != "" { 264 - return peerID 265 - } 266 - key := strings.TrimSpace(msg.ConversationKey) 267 - if !strings.HasPrefix(strings.ToLower(key), "maep:") { 268 - return "" 269 - } 270 - return strings.TrimSpace(key[len("maep:"):]) 271 - } 272 - 273 - func extractMAEPMentionPeerIDs(text string) []string { 274 - text = strings.TrimSpace(text) 275 - if text == "" { 276 - return nil 277 - } 278 - matches := maepMentionTokenPattern.FindAllString(text, -1) 279 - if len(matches) == 0 { 280 - return nil 281 - } 282 - seen := map[string]bool{} 283 - out := make([]string, 0, len(matches)) 284 - for _, raw := range matches { 285 - normalizedNodeID, peerID := splitMAEPNodeID(raw) 286 - _ = normalizedNodeID 287 - peerID = strings.TrimSpace(peerID) 288 - if peerID == "" { 289 - continue 290 - } 291 - key := strings.ToLower(peerID) 292 - if seen[key] { 293 - continue 294 - } 295 - seen[key] = true 296 - out = append(out, peerID) 297 - } 298 - sort.Slice(out, func(i, j int) bool { 299 - return strings.ToLower(out[i]) < strings.ToLower(out[j]) 300 - }) 301 - return out 302 - } 303 - 304 161 func slackContactIDFromUser(teamID, userID string) string { 305 162 teamID = normalizeSlackID(teamID) 306 163 userID = normalizeSlackID(userID) ··· 393 250 } 394 251 applyObservedTelegramMerge(&existing, candidate) 395 252 applyObservedSlackMerge(&existing, candidate) 396 - if strings.TrimSpace(existing.MAEPNodeID) == "" { 397 - existing.MAEPNodeID = strings.TrimSpace(candidate.MAEPNodeID) 398 - } 399 - if strings.TrimSpace(existing.MAEPDialAddress) == "" { 400 - existing.MAEPDialAddress = strings.TrimSpace(candidate.MAEPDialAddress) 401 - } 402 253 existing.LastInteractionAt = &lastInteraction 403 254 _, err := s.UpsertContact(ctx, existing, now) 404 255 return err ··· 414 265 SlackUserID: normalizeSlackID(candidate.SlackUserID), 415 266 SlackDMChannelID: normalizeSlackID(candidate.SlackDMChannelID), 416 267 SlackChannelIDs: normalizeStringSlice(candidate.SlackChannelIDs), 417 - MAEPNodeID: strings.TrimSpace(candidate.MAEPNodeID), 418 - MAEPDialAddress: strings.TrimSpace(candidate.MAEPDialAddress), 419 268 LastInteractionAt: &lastInteraction, 420 269 } 421 270 applyObservedTelegramMerge(&contact, candidate)
+6 -59
contacts/bus_observe_test.go
··· 39 39 MentionUsers: []string{"@alice", "bob"}, 40 40 }, 41 41 } 42 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now); err != nil { 42 + if err := svc.ObserveInboundBusMessage(ctx, msg, now); err != nil { 43 43 t.Fatalf("ObserveInboundBusMessage() error = %v", err) 44 44 } 45 45 ··· 94 94 FromUsername: "neo", 95 95 }, 96 96 } 97 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now); err != nil { 97 + if err := svc.ObserveInboundBusMessage(ctx, msg, now); err != nil { 98 98 t.Fatalf("ObserveInboundBusMessage(first) error = %v", err) 99 99 } 100 100 item, ok, err := svc.GetContact(ctx, "tg:@neo") ··· 109 109 } 110 110 111 111 msg.ConversationKey = "tg:90099" 112 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now.Add(1*time.Minute)); err != nil { 112 + if err := svc.ObserveInboundBusMessage(ctx, msg, now.Add(1*time.Minute)); err != nil { 113 113 t.Fatalf("ObserveInboundBusMessage(second) error = %v", err) 114 114 } 115 115 item, ok, err = svc.GetContact(ctx, "tg:@neo") ··· 141 141 MentionUsers: []string{"U100", "U200"}, 142 142 }, 143 143 } 144 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now); err != nil { 144 + if err := svc.ObserveInboundBusMessage(ctx, msg, now); err != nil { 145 145 t.Fatalf("ObserveInboundBusMessage() error = %v", err) 146 146 } 147 147 ··· 198 198 FromUserRef: "U300", 199 199 }, 200 200 } 201 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now); err != nil { 201 + if err := svc.ObserveInboundBusMessage(ctx, msg, now); err != nil { 202 202 t.Fatalf("ObserveInboundBusMessage(first) error = %v", err) 203 203 } 204 204 item, ok, err := svc.GetContact(ctx, "slack:T111:U300") ··· 213 213 } 214 214 215 215 msg.ConversationKey = "slack:T111:D90002" 216 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now.Add(1*time.Minute)); err != nil { 216 + if err := svc.ObserveInboundBusMessage(ctx, msg, now.Add(1*time.Minute)); err != nil { 217 217 t.Fatalf("ObserveInboundBusMessage(second) error = %v", err) 218 218 } 219 219 item, ok, err = svc.GetContact(ctx, "slack:T111:U300") ··· 225 225 } 226 226 if item.SlackDMChannelID != "D90001" { 227 227 t.Fatalf("slack_dm_channel_id should not be overwritten: got %q want %q", item.SlackDMChannelID, "D90001") 228 - } 229 - } 230 - 231 - func TestObserveInboundBusMessage_MAEPSenderAndMention(t *testing.T) { 232 - ctx := context.Background() 233 - store := NewFileStore(t.TempDir()) 234 - svc := NewService(store) 235 - now := time.Date(2026, 2, 10, 10, 0, 0, 0, time.UTC) 236 - 237 - payloadBase64, err := busruntime.EncodeMessageEnvelope( 238 - busruntime.TopicChatMessage, 239 - busruntime.MessageEnvelope{ 240 - MessageID: "maep:test:1", 241 - Text: "hello maep:12D3KooWPeerB and maep:12D3KooWPeerB", 242 - SentAt: now.Format(time.RFC3339), 243 - SessionID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", 244 - }, 245 - ) 246 - if err != nil { 247 - t.Fatalf("EncodeMessageEnvelope() error = %v", err) 248 - } 249 - msg := busruntime.BusMessage{ 250 - Direction: busruntime.DirectionInbound, 251 - Channel: busruntime.ChannelMAEP, 252 - Topic: busruntime.TopicChatMessage, 253 - ConversationKey: "maep:12D3KooWPeerA", 254 - ParticipantKey: "12D3KooWPeerA", 255 - IdempotencyKey: "msg:maep_test_1", 256 - PayloadBase64: payloadBase64, 257 - CreatedAt: now, 258 - } 259 - if err := svc.ObserveInboundBusMessage(ctx, msg, nil, now); err != nil { 260 - t.Fatalf("ObserveInboundBusMessage() error = %v", err) 261 - } 262 - 263 - for _, peerID := range []string{"12D3KooWPeerA", "12D3KooWPeerB"} { 264 - contactID := "maep:" + peerID 265 - item, ok, err := svc.GetContact(ctx, contactID) 266 - if err != nil { 267 - t.Fatalf("GetContact(%s) error = %v", contactID, err) 268 - } 269 - if !ok { 270 - t.Fatalf("GetContact(%s) expected ok=true", contactID) 271 - } 272 - if item.Channel != ChannelMAEP { 273 - t.Fatalf("channel mismatch for %s: got %s want %s", contactID, item.Channel, ChannelMAEP) 274 - } 275 - if item.Kind != KindAgent { 276 - t.Fatalf("kind mismatch for %s: got %s want %s", contactID, item.Kind, KindAgent) 277 - } 278 - if item.LastInteractionAt == nil || !item.LastInteractionAt.Equal(now) { 279 - t.Fatalf("last_interaction_at mismatch for %s: got=%v want=%v", contactID, item.LastInteractionAt, now) 280 - } 281 228 } 282 229 } 283 230
+5 -7
contacts/invariants_test.go
··· 34 34 now := time.Date(2026, 2, 8, 21, 0, 0, 0, time.UTC) 35 35 36 36 if _, err := svc.UpsertContact(ctx, Contact{ 37 - ContactID: "maep:12D3KooWInv", 38 - Kind: KindAgent, 39 - Channel: ChannelMAEP, 40 - MAEPNodeID: "maep:12D3KooWInv", 41 - MAEPDialAddress: "/ip4/127.0.0.1/tcp/4021/p2p/12D3KooWInv", 37 + ContactID: "tg:10086", 38 + Kind: KindHuman, 39 + Channel: ChannelTelegram, 40 + TGPrivateChatID: 10086, 42 41 }, now); err != nil { 43 42 t.Fatalf("UpsertContact() error = %v", err) 44 43 } ··· 46 45 sender := &mockSender{accepted: true} 47 46 payload := base64.RawURLEncoding.EncodeToString([]byte("hello")) 48 47 decision := ShareDecision{ 49 - ContactID: "maep:12D3KooWInv", 50 - PeerID: "12D3KooWInv", 48 + ContactID: "tg:10086", 51 49 ItemID: "manual_item_1", 52 50 ContentType: "text/plain", 53 51 PayloadBase64: payload,
-21
contacts/service.go
··· 222 222 return ShareOutcome{}, fmt.Errorf("contact not found: %s", decision.ContactID) 223 223 } 224 224 decision.ContactID = contact.ContactID 225 - 226 - if strings.TrimSpace(decision.PeerID) == "" { 227 - decision.PeerID = resolveMAEPPeerID(contact) 228 - } 229 225 decision.ContentType = strings.TrimSpace(decision.ContentType) 230 226 if decision.ContentType == "" { 231 227 decision.ContentType = "application/json" ··· 348 344 } 349 345 if hasTelegramTarget(contact) { 350 346 return ChannelTelegram, nil 351 - } 352 - if strings.TrimSpace(decision.PeerID) != "" || resolveMAEPPeerID(contact) != "" { 353 - return ChannelMAEP, nil 354 347 } 355 348 return "", fmt.Errorf("unable to resolve delivery channel for contact_id=%s", contact.ContactID) 356 349 } ··· 542 535 } 543 536 } 544 537 } 545 - if v := strings.TrimSpace(contact.MAEPNodeID); v != "" { 546 - nodeID, _ := splitMAEPNodeID(v) 547 - return nodeID 548 - } 549 538 if contact.Channel == ChannelTelegram { 550 539 ids := append([]int64(nil), contact.TGGroupChatIDs...) 551 540 sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) ··· 554 543 return "tg:" + strconv.FormatInt(id, 10) 555 544 } 556 545 } 557 - } 558 - return "" 559 - } 560 - 561 - func resolveMAEPPeerID(contact Contact) string { 562 - if _, peerID := splitMAEPNodeID(contact.MAEPNodeID); peerID != "" { 563 - return peerID 564 - } 565 - if _, peerID := splitMAEPNodeID(contact.ContactID); peerID != "" { 566 - return peerID 567 546 } 568 547 return "" 569 548 }
+167
docs/feat/feat_20260223_remove_maep.md
··· 1 + --- 2 + date: 2026-02-23 3 + title: Hard Remove MAEP(实施方案) 4 + status: draft 5 + --- 6 + 7 + # Hard Remove MAEP(实施方案) 8 + 9 + ## 1) 背景与目标 10 + 11 + 当前仓库中,MAEP 虽然默认运行关闭(`server.with_maep=false`、`telegram.with_maep=false`),但代码层面深度耦合到: 12 + - CLI 命令与参数 13 + - Telegram/Daemon runtime 14 + - Bus channel/adapters 15 + - Contacts 路由与发送器 16 + - 安装流程与本地状态目录 17 + 18 + 本方案目标是**硬移除 MAEP**,将系统收敛为: 19 + - Telegram + Slack 两条通道 20 + - Contacts 仅在 Telegram/Slack 之间路由 21 + - Bus 仅保留 telegram/slack/discord(保留 discord 预埋) 22 + 23 + ## 2) 第一性原则 24 + 25 + - 只保留当前真实需要的能力,删掉长期未使用的复杂度。 26 + - 一个能力不再支持,就同时删除其入口、实现、测试、配置与文档,避免“僵尸代码”。 27 + - 拆除以“可编译、可测试、可回滚”为边界,分批提交。 28 + 29 + ## 3) 非目标 30 + 31 + - 不在本次重构中引入新的 P2P 协议替代 MAEP。 32 + - 不重做 contacts 业务模型(仅删除 MAEP 相关字段/分支)。 33 + - 不改动 heartbeat/memory 主流程(除非受编译依赖牵连)。 34 + 35 + ## 4) 现状摘要(耦合面) 36 + 37 + - CLI 入口: 38 + - `mistermorph maep` 子命令 39 + - `telegram --with-maep`、`serve --with-maep` 40 + - Runtime: 41 + - Telegram 内嵌 MAEP node + MAEP 入站 auto-reply 流程 42 + - Daemon 可选启动 MAEP listener 43 + - Bus: 44 + - `ChannelMAEP`、`BuildMAEPPeerConversationKey(...)` 45 + - `internal/bus/adapters/maep/*` 46 + - Contacts: 47 + - `ChannelMAEP` 路由分支 48 + - `maep_node_id` / `maep_dial_address` 字段 49 + - `contactsruntime` 发送器支持 MAEP publish 50 + - Install/State: 51 + - install 时自动初始化 MAEP identity 52 + - `statepaths.MAEPDir()` 53 + - 依赖: 54 + - `go-libp2p` / `go-multiaddr` 及其大量传递依赖 55 + 56 + ## 5) 方案总览 57 + 58 + 按 6 个批次实施,每批均要求:`go test ./...` 通过后再进入下一批。 59 + 60 + ### 批次 A:入口与配置下线 61 + 62 + - 删除 `cmd/mistermorph/root.go` 中 `maepcmd.New()` 注册。 63 + - 删除 `cmd/mistermorph/telegramcmd/command.go`: 64 + - `--with-maep` 65 + - `--maep-listen` 66 + - 对应输入字段映射 67 + - 删除 `cmd/mistermorph/daemoncmd/serve.go`: 68 + - `--with-maep` 69 + - `--maep-listen` 70 + - 启动 embedded MAEP 的分支 71 + - 删除默认配置与样例配置中的: 72 + - `maep.*` 73 + - `server.with_maep` 74 + - `telegram.with_maep` 75 + 76 + ### 批次 B:Telegram Runtime 去 MAEP 化 77 + 78 + - 删除 `internal/channelruntime/telegram/runtime.go` 中: 79 + - `withMAEP` 分支 80 + - `maepEventCh` 与 MAEP bus dispatch 81 + - MAEP inbound feedback/session 限流/auto-reply 逻辑 82 + - 删除 `internal/channelruntime/telegram/runtime_task.go` 中: 83 + - `runMAEPTask(...)` 84 + - `buildMAEPRegistry(...)` 85 + - MAEP prompt policy 注入分支(若仅用于 MAEP) 86 + - 删除 MAEP 专属 prompt 模板与渲染器(若无其他引用)。 87 + 88 + ### 批次 C:Bus 与 Adapter 收敛 89 + 90 + - 删除 `internal/bus/message.go` 的 `ChannelMAEP`。 91 + - 删除 `internal/bus/conversation_key.go` 的 MAEP builder 与 MAEP prefix 分支。 92 + - 删除目录 `internal/bus/adapters/maep/` 及其测试。 93 + - 修复 `internal/bus/adapters/inbound_flow.go` 等共享校验里对 MAEP 的合法值引用。 94 + 95 + ### 批次 D:Contacts 与发送路径收敛 96 + 97 + - 删除 `contacts/types.go`: 98 + - `ChannelMAEP` 99 + - `MAEPNodeID` / `MAEPDialAddress` 100 + - `ShareDecision.PeerID`(若确认仅用于 MAEP) 101 + - 删除 `contacts/service.go` 中 MAEP 路由回退分支。 102 + - 删除 `internal/contactsruntime/sender.go` 中: 103 + - MAEP publish/delivery/node 初始化逻辑 104 + - `MAEPDir` 选项 105 + - 收敛 `tools/builtin/contacts_send.go` 文案与参数说明(移除 MAEP 示例)。 106 + 107 + ### 批次 E:状态与安装流程收敛 108 + 109 + - 删除 `internal/statepaths/statepaths.go` 的 `MAEPDir()`。 110 + - 删除 install 中 MAEP identity 初始化逻辑。 111 + - 删除 `cmd/mistermorph/registry.go` / runtime snapshot 中 `MAEPDir` 字段透传。 112 + 113 + ### 批次 F:删除 MAEP 代码与依赖 114 + 115 + - 删除目录: 116 + - `maep/` 117 + - `cmd/mistermorph/maepcmd/` 118 + - `internal/maepruntime/` 119 + - 清理依赖并执行 `go mod tidy`,移除 libp2p/multiaddr 相关依赖。 120 + - 清理文档入口: 121 + - `docs/README.md` 中 MAEP 项 122 + - 与 MAEP 强绑定的实现文档改为“removed”说明或删除。 123 + 124 + ## 6) 数据兼容与迁移策略 125 + 126 + ### 6.1 配置兼容 127 + 128 + - 对已存在的 `maep.*`、`*.with_maep` 键采取“忽略但不报错”策略(由 viper 天然容忍未知键)。 129 + - CLI 层直接移除参数,用户若继续传入会得到参数不存在错误(预期行为)。 130 + 131 + ### 6.2 Contacts 存量数据 132 + 133 + - 历史 `contacts` 文件中若存在 MAEP 字段: 134 + - 读取时忽略未知字段(YAML 解码容忍) 135 + - 路由时不再走 MAEP,若联系人仅有 MAEP reachability,则返回明确错误:`unable to resolve delivery channel` 136 + - 不做自动数据迁移脚本(本次目标是代码硬移除)。 137 + 138 + ## 7) 风险与控制 139 + 140 + - 风险 1:删除 `ChannelMAEP` 触发 bus/contacts 广泛编译错误。 141 + 控制:按批次 C -> D 处理,逐层收敛并保持每批可测试。 142 + 143 + - 风险 2:`contacts_send` 行为变化导致已有 MAEP 联系人发送失败。 144 + 控制:明确错误信息,更新工具描述和文档。 145 + 146 + - 风险 3:依赖移除后触发构建脚本或文档引用残留。 147 + 控制:最终批次执行 repo 级 grep 与全量测试。 148 + 149 + ## 8) 验收标准(Definition of Done) 150 + 151 + - `go test ./...` 全绿。 152 + - `mistermorph --help` 不再出现 `maep` 子命令。 153 + - `mistermorph telegram --help` / `mistermorph serve --help` 不再出现 MAEP 参数。 154 + - 仓库中不再存在 MAEP runtime 代码路径(文档历史记录除外)。 155 + - `go.mod` 不再依赖 `go-libp2p`、`go-multiaddr`。 156 + - `contacts_send` 文案与行为仅覆盖 Telegram/Slack。 157 + 158 + ## 9) 提交策略 159 + 160 + - 推荐按批次拆成 4-6 个 commit: 161 + 1. CLI/config 下线 162 + 2. Telegram/Daemon runtime 去 MAEP 163 + 3. Bus + Contacts + sender 收敛 164 + 4. 删除 MAEP 目录与依赖清理 165 + 5. 文档与说明收尾 166 + 167 + 这样每个 commit 都可独立 review 与回滚。
-1
docs/zh-CN/README.md
··· 23 23 这个项目值得关注的原因: 24 24 25 25 - 🧩 **可复用的 Go 核心**:既能把 Agent 当 CLI 运行,也能以库或子进程的方式嵌入到其他应用。 26 - - 🤝 **Mesh Agent Exchange Protocol(MAEP)**:如果你和伙伴各自运行多个 Agent,且希望它们互相通信,可以使用 MAEP。它是一个带信任状态与审计轨迹的 P2P 协议(见 [../maep.md](../maep.md),WIP)。 27 26 - 🔒 **严肃的默认安全策略**:基于 profile 的凭据注入、Guard 脱敏、出站策略控制、带审计轨迹的异步审批(见 [../security.md](../security.md))。 28 27 - 🧰 **实用的 Skills 系统**:可从 `file_state_dir/skills` 发现并注入 `SKILL.md`,支持简单的 on/off 控制(见 [../skills.md](../skills.md))。 29 28 - 📚 **对新手友好**:这是一个以学习为导向的 Agent 项目;`docs/` 里有详细设计文档,也提供了 `--inspect-prompt`、`--inspect-request` 等实用调试工具。
+3 -81
go.mod
··· 5 5 toolchain go1.24.13 6 6 7 7 require ( 8 - github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 9 8 github.com/google/uuid v1.6.0 10 9 github.com/gorilla/websocket v1.5.3 11 - github.com/libp2p/go-libp2p v0.44.0 12 10 github.com/lyricat/goutils v1.2.3 13 - github.com/multiformats/go-multiaddr v0.16.0 14 11 github.com/quailyquaily/uniai v0.1.2 15 12 github.com/spf13/cobra v1.8.1 16 13 github.com/spf13/viper v1.19.0 14 + golang.org/x/crypto v0.47.0 17 15 golang.org/x/net v0.49.0 18 16 golang.org/x/sys v0.40.0 19 17 golang.org/x/term v0.39.0 ··· 24 22 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect 25 23 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect 26 24 github.com/aws/aws-sdk-go v1.55.8 // indirect 27 - github.com/benbjohnson/clock v1.3.5 // indirect 28 - github.com/beorn7/perks v1.0.1 // indirect 29 - github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 - github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect 31 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 32 - github.com/flynn/noise v1.1.0 // indirect 33 - github.com/francoispqt/gojay v1.2.13 // indirect 34 25 github.com/fsnotify/fsnotify v1.7.0 // indirect 26 + github.com/google/go-cmp v0.7.0 // indirect 35 27 github.com/hashicorp/hcl v1.0.0 // indirect 36 - github.com/huin/goupnp v1.3.0 // indirect 37 28 github.com/inconshreveable/mousetrap v1.1.0 // indirect 38 - github.com/ipfs/go-cid v0.5.0 // indirect 39 - github.com/jackpal/go-nat-pmp v1.0.2 // indirect 40 - github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect 41 29 github.com/jmespath/go-jmespath v0.4.0 // indirect 42 - github.com/klauspost/compress v1.18.0 // indirect 43 - github.com/klauspost/cpuid/v2 v2.2.10 // indirect 44 - github.com/koron/go-ssdp v0.0.6 // indirect 45 - github.com/libp2p/go-buffer-pool v0.1.0 // indirect 46 - github.com/libp2p/go-flow-metrics v0.2.0 // indirect 47 - github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect 48 - github.com/libp2p/go-msgio v0.3.0 // indirect 49 - github.com/libp2p/go-netroute v0.3.0 // indirect 50 - github.com/libp2p/go-reuseport v0.4.0 // indirect 51 - github.com/libp2p/go-yamux/v5 v5.0.1 // indirect 52 30 github.com/magiconair/properties v1.8.7 // indirect 53 - github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect 54 - github.com/miekg/dns v1.1.66 // indirect 55 - github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect 56 - github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect 57 - github.com/minio/sha256-simd v1.0.1 // indirect 58 31 github.com/mitchellh/mapstructure v1.5.0 // indirect 59 - github.com/mr-tron/base58 v1.2.0 // indirect 60 - github.com/multiformats/go-base32 v0.1.0 // indirect 61 - github.com/multiformats/go-base36 v0.2.0 // indirect 62 - github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect 63 - github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect 64 - github.com/multiformats/go-multibase v0.2.0 // indirect 65 - github.com/multiformats/go-multicodec v0.9.1 // indirect 66 - github.com/multiformats/go-multihash v0.2.3 // indirect 67 - github.com/multiformats/go-multistream v0.6.1 // indirect 68 - github.com/multiformats/go-varint v0.0.7 // indirect 69 - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 70 32 github.com/openai/openai-go/v3 v3.2.0 // indirect 71 - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 72 33 github.com/pelletier/go-toml/v2 v2.2.2 // indirect 73 - github.com/pion/datachannel v1.5.10 // indirect 74 - github.com/pion/dtls/v2 v2.2.12 // indirect 75 - github.com/pion/dtls/v3 v3.0.6 // indirect 76 - github.com/pion/ice/v4 v4.0.10 // indirect 77 - github.com/pion/interceptor v0.1.40 // indirect 78 - github.com/pion/logging v0.2.3 // indirect 79 - github.com/pion/mdns/v2 v2.0.7 // indirect 80 - github.com/pion/randutil v0.1.0 // indirect 81 - github.com/pion/rtcp v1.2.15 // indirect 82 - github.com/pion/rtp v1.8.19 // indirect 83 - github.com/pion/sctp v1.8.39 // indirect 84 - github.com/pion/sdp/v3 v3.0.13 // indirect 85 - github.com/pion/srtp/v3 v3.0.6 // indirect 86 - github.com/pion/stun v0.6.1 // indirect 87 - github.com/pion/stun/v3 v3.0.0 // indirect 88 - github.com/pion/transport/v2 v2.2.10 // indirect 89 - github.com/pion/transport/v3 v3.0.7 // indirect 90 - github.com/pion/turn/v4 v4.0.2 // indirect 91 - github.com/pion/webrtc/v4 v4.1.2 // indirect 92 - github.com/prometheus/client_golang v1.22.0 // indirect 93 - github.com/prometheus/client_model v0.6.2 // indirect 94 - github.com/prometheus/common v0.64.0 // indirect 95 - github.com/prometheus/procfs v0.16.1 // indirect 96 - github.com/quic-go/qpack v0.5.1 // indirect 97 - github.com/quic-go/quic-go v0.55.0 // indirect 98 - github.com/quic-go/webtransport-go v0.9.0 // indirect 99 34 github.com/sagikazarmark/locafero v0.4.0 // indirect 100 35 github.com/sagikazarmark/slog-shim v0.1.0 // indirect 101 36 github.com/shopspring/decimal v1.4.0 // indirect 102 37 github.com/sourcegraph/conc v0.3.0 // indirect 103 - github.com/spaolacci/murmur3 v1.1.0 // indirect 104 38 github.com/spf13/afero v1.11.0 // indirect 105 39 github.com/spf13/cast v1.6.0 // indirect 106 40 github.com/spf13/pflag v1.0.5 // indirect ··· 109 43 github.com/tidwall/match v1.1.1 // indirect 110 44 github.com/tidwall/pretty v1.2.1 // indirect 111 45 github.com/tidwall/sjson v1.2.5 // indirect 112 - github.com/wlynxg/anet v0.0.5 // indirect 113 - go.uber.org/dig v1.19.0 // indirect 114 - go.uber.org/fx v1.24.0 // indirect 115 - go.uber.org/mock v0.5.2 // indirect 116 46 go.uber.org/multierr v1.11.0 // indirect 117 - go.uber.org/zap v1.27.0 // indirect 118 - golang.org/x/crypto v0.47.0 // indirect 119 47 golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect 120 - golang.org/x/mod v0.31.0 // indirect 121 - golang.org/x/sync v0.19.0 // indirect 122 - golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect 123 48 golang.org/x/text v0.33.0 // indirect 124 - golang.org/x/time v0.12.0 // indirect 125 - golang.org/x/tools v0.40.0 // indirect 126 - google.golang.org/protobuf v1.36.6 // indirect 127 49 gopkg.in/ini.v1 v1.67.0 // indirect 128 - lukechampine.com/blake3 v1.4.1 // indirect 50 + gopkg.in/yaml.v2 v2.4.0 // indirect 129 51 )
-392
go.sum
··· 1 - cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 - cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 - cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 - cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 - dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 - dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 - dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 - dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 - git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 1 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= 11 2 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 12 3 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= ··· 15 6 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= 16 7 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 17 8 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 18 - github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 19 - github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 20 9 github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= 21 10 github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= 22 - github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= 23 - github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 24 - github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 25 - github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 26 - github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 - github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 28 - github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 29 - github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 30 - github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 31 - github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 32 - github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 33 11 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 34 - github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= 35 - github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= 36 12 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 13 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 14 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 39 15 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 - github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= 41 - github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= 42 - github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= 43 - github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 44 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 45 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 46 - github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 47 - github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 48 - github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= 49 - github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= 50 - github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 51 - github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 52 16 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 53 17 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 54 - github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 55 18 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 56 19 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 57 - github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 58 - github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 59 - github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 60 - github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 61 20 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 62 21 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 63 22 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 64 - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 65 - github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 66 - github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 67 - github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 68 - github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 - github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 - github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 71 - github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 72 23 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 73 24 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 74 - github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 75 - github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 76 - github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 77 - github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 78 25 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 79 26 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 80 - github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 81 - github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 82 - github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 83 27 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 84 28 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 85 - github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 86 - github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 87 29 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 88 30 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 89 - github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 90 - github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 91 31 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 92 32 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 93 - github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 94 - github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 95 - github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 96 - github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 97 - github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= 98 - github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= 99 - github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 100 33 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 101 34 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 102 35 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 103 36 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 104 - github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 105 - github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 106 - github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 107 - github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 108 - github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 109 - github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 110 - github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 111 - github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= 112 - github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= 113 - github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 114 - github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 115 37 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 116 38 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 117 - github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 118 - github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 119 - github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 120 39 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 121 40 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 122 41 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 123 42 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 124 - github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 125 - github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 126 - github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= 127 - github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= 128 - github.com/libp2p/go-libp2p v0.44.0 h1:5Gtt8OrF8yiXmH+Mx4+/iBeFRMK1TY3a8OrEBDEqAvs= 129 - github.com/libp2p/go-libp2p v0.44.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc= 130 - github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= 131 - github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= 132 - github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 133 - github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 134 - github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= 135 - github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= 136 - github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= 137 - github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= 138 - github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= 139 - github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= 140 - github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= 141 - github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= 142 - github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 143 43 github.com/lyricat/goutils v1.2.3 h1:bJCYygnCYwELtXrzeA/oW0Xl1aMRMutpzyWqfF5AvJI= 144 44 github.com/lyricat/goutils v1.2.3/go.mod h1:AscmPHLrB2accCEVP4gSI6y3ezcud3zHM1w3t7M/jNU= 145 45 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 146 46 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 147 - github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 148 - github.com/marcopolo/simnet v0.0.1 h1:rSMslhPz6q9IvJeFWDoMGxMIrlsbXau3NkuIXHGJxfg= 149 - github.com/marcopolo/simnet v0.0.1/go.mod h1:WDaQkgLAjqDUEBAOXz22+1j6wXKfGlC5sD5XWt3ddOs= 150 - github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= 151 - github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= 152 - github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 153 - github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 154 - github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= 155 - github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= 156 - github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= 157 - github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= 158 - github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= 159 - github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= 160 - github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= 161 - github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= 162 - github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= 163 - github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= 164 - github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 165 - github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 166 47 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 167 48 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 168 - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 169 - github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 170 - github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 171 - github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 172 - github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 173 - github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 174 - github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 175 - github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 176 - github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 177 - github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= 178 - github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= 179 - github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= 180 - github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= 181 - github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= 182 - github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 183 - github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 184 - github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 185 - github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 186 - github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= 187 - github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= 188 - github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= 189 - github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 190 - github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 191 - github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= 192 - github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= 193 - github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 194 - github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 195 - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 196 - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 197 - github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 198 - github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 199 49 github.com/openai/openai-go/v3 v3.2.0 h1:2AbqFUCsoW2pm/2pUtPRuwK89dnoGHaQokzWsfoQO/U= 200 50 github.com/openai/openai-go/v3 v3.2.0/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs= 201 - github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 202 - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 203 - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 204 51 github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 205 52 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 206 - github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= 207 - github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= 208 - github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= 209 - github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= 210 - github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= 211 - github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= 212 - github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= 213 - github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= 214 - github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= 215 - github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= 216 - github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= 217 - github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 218 - github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= 219 - github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= 220 - github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= 221 - github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= 222 - github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 223 - github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 224 - github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= 225 - github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= 226 - github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= 227 - github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= 228 - github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= 229 - github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= 230 - github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= 231 - github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= 232 - github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= 233 - github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= 234 - github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= 235 - github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= 236 - github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= 237 - github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= 238 - github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= 239 - github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= 240 - github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= 241 - github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= 242 - github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 243 - github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 244 - github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= 245 - github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= 246 - github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= 247 - github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= 248 53 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 249 54 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 250 - github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 251 55 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 252 56 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 253 57 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 254 - github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 255 - github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 256 - github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 257 - github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 258 - github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 259 - github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 260 - github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 261 - github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 262 - github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 263 - github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 264 - github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 265 - github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 266 58 github.com/quailyquaily/uniai v0.1.2 h1:IjwudUAVCbc/5C8HxMJ+EMbkqddpoO6qFlerkYgIiaY= 267 59 github.com/quailyquaily/uniai v0.1.2/go.mod h1:C3kQuLcZ+QvU1+uRmRXkHx3Jo8gaZxa6Da54aUI9nj4= 268 - github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 269 - github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 270 - github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= 271 - github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= 272 - github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= 273 - github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= 274 60 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 275 61 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 276 - github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 277 62 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 278 63 github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 279 64 github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 280 65 github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 281 66 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 282 - github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 283 67 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 284 68 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 285 - github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 286 - github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 287 - github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 288 - github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 289 - github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 290 - github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 291 - github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 292 - github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 293 - github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 294 - github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 295 - github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 296 - github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 297 - github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 298 - github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 299 - github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 300 - github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 301 - github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 302 - github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 303 - github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 304 - github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 305 - github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 306 - github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 307 - github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 308 69 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 309 70 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 310 - github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 311 - github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 312 - github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 313 71 github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 314 72 github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 315 73 github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= ··· 324 82 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 325 83 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 326 84 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 327 - github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 328 85 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 329 86 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 330 - github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 331 87 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 332 88 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 333 89 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 334 90 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 335 91 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 336 92 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 337 - github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 338 93 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 339 94 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 340 95 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= ··· 345 100 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 346 101 github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 347 102 github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 348 - github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 349 - github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 350 - github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 351 - github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= 352 - github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 353 - github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 354 - go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 355 - go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= 356 - go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= 357 - go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= 358 - go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= 359 - go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 360 - go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 361 - go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 362 - go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 363 103 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 364 104 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 365 - go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 366 - go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 367 - go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 368 - golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 369 - golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 370 - golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 371 - golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 372 - golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 373 - golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 374 - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 375 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 376 - golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 377 - golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 378 - golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 379 105 golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= 380 106 golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 381 - golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 382 107 golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= 383 108 golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 384 - golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 385 - golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 386 - golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 387 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 388 - golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 389 - golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 390 - golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 391 - golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 392 - golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 393 - golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 394 - golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 395 - golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 396 - golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 397 - golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 398 - golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 399 - golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 400 - golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 401 - golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 402 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 403 - golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 404 - golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 405 - golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 406 - golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 407 - golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 408 - golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 409 109 golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= 410 110 golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 411 - golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 412 - golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 413 - golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 414 - golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 415 - golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 416 - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 - golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 419 - golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 420 - golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 421 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 422 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 423 - golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 424 - golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 425 - golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 426 - golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 427 - golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 428 - golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 429 - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 430 - golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 - golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 - golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 - golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 435 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 436 - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 437 - golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 438 - golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 439 - golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 440 - golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 441 - golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 442 111 golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= 443 112 golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 444 - golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= 445 - golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 446 - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 447 - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 448 - golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 449 - golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 450 - golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 451 - golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 452 - golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 453 113 golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= 454 114 golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 455 - golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 456 - golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 457 - golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 458 - golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 459 - golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 460 - golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 461 - golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 462 - golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 463 115 golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= 464 116 golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 465 - golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 466 - golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 467 - golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 468 - golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 469 - golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 470 - golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 471 - golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 472 - golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 473 - golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 474 - golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 475 - golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 476 - golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 477 - golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= 478 - golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 479 - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 480 - google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 481 - google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 482 - google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 483 - google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 484 - google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 485 - google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 486 - google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 487 - google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 488 - google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 489 - google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 490 - google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 491 - google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 492 - google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 493 - google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 494 - google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 495 - google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 496 - google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 497 - google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 498 117 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 499 118 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 500 119 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 501 - gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 502 120 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 503 121 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 504 - gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 505 - gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 506 122 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 507 123 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 508 124 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 509 125 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 510 126 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 511 127 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 512 - grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 513 - honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 514 - honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 515 - honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 516 - lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 517 - lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= 518 - sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 519 - sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
-1
integration/channel_bots.go
··· 113 113 GroupTriggerMode: strings.TrimSpace(r.opts.GroupTriggerMode), 114 114 AddressingConfidenceThreshold: r.opts.AddressingConfidenceThreshold, 115 115 AddressingInterjectThreshold: r.opts.AddressingInterjectThreshold, 116 - WithMAEP: false, 117 116 PollTimeout: r.opts.PollTimeout, 118 117 TaskTimeout: r.opts.TaskTimeout, 119 118 MaxConcurrency: r.opts.MaxConcurrency,
-6
integration/defaults.go
··· 59 59 v.SetDefault("skills.mode", "on") 60 60 v.SetDefault("skills.dir_name", "skills") 61 61 62 - // MAEP. 63 - v.SetDefault("maep.dir_name", "maep") 64 - v.SetDefault("maep.listen_addrs", []string{}) 65 - 66 62 // Bus. 67 63 v.SetDefault("bus.max_inflight", 1024) 68 64 ··· 74 70 // Daemon server. 75 71 v.SetDefault("server.listen", "127.0.0.1:8787") 76 72 v.SetDefault("server.max_queue", 100) 77 - v.SetDefault("server.with_maep", false) 78 73 79 74 // Submit client. 80 75 v.SetDefault("submit.wait", false) ··· 86 81 v.SetDefault("telegram.addressing_confidence_threshold", 0.6) 87 82 v.SetDefault("telegram.addressing_interject_threshold", 0.3) 88 83 v.SetDefault("telegram.max_concurrency", 3) 89 - v.SetDefault("telegram.with_maep", false) 90 84 91 85 // Slack. 92 86 v.SetDefault("slack.base_url", "https://slack.com/api")
-1
integration/registry.go
··· 148 148 r.Register(builtin.NewContactsSendTool(builtin.ContactsSendToolOptions{ 149 149 Enabled: true, 150 150 ContactsDir: cfg.ContactsDir, 151 - MAEPDir: cfg.MAEPDir, 152 151 TelegramBotToken: strings.TrimSpace(cfg.TelegramBotToken), 153 152 TelegramBaseURL: strings.TrimSpace(cfg.TelegramBaseURL), 154 153 SlackBotToken: strings.TrimSpace(cfg.SlackBotToken),
-1
integration/runtime_snapshot.go
··· 63 63 TODOPathWIP string 64 64 TODOPathDone string 65 65 ContactsDir string 66 - MAEPDir string 67 66 TelegramBotToken string 68 67 TelegramBaseURL string 69 68 SlackBotToken string
-1
integration/runtime_snapshot_loader.go
··· 103 103 TODOPathWIP: pathutil.ResolveStateFile(fileStateDir, statepaths.TODOWIPFilename), 104 104 TODOPathDone: pathutil.ResolveStateFile(fileStateDir, statepaths.TODODONEFilename), 105 105 ContactsDir: pathutil.ResolveStateChildDir(fileStateDir, strings.TrimSpace(v.GetString("contacts.dir_name")), "contacts"), 106 - MAEPDir: pathutil.ResolveStateChildDir(fileStateDir, strings.TrimSpace(v.GetString("maep.dir_name")), "maep"), 107 106 TelegramBotToken: strings.TrimSpace(v.GetString("telegram.bot_token")), 108 107 TelegramBaseURL: "https://api.telegram.org", 109 108 SlackBotToken: strings.TrimSpace(v.GetString("slack.bot_token")),
+1 -1
internal/bus/adapters/inbound_flow.go
··· 39 39 } 40 40 channel := strings.ToLower(strings.TrimSpace(opts.Channel)) 41 41 switch channel { 42 - case channels.Telegram, channels.MAEP, channels.Slack, channels.Discord: 42 + case channels.Telegram, channels.Slack, channels.Discord: 43 43 default: 44 44 return nil, fmt.Errorf("unsupported channel: %q", opts.Channel) 45 45 }
-60
internal/bus/adapters/maep/delivery.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "strings" 7 - 8 - busruntime "github.com/quailyquaily/mistermorph/internal/bus" 9 - maepproto "github.com/quailyquaily/mistermorph/maep" 10 - ) 11 - 12 - type DataPusher interface { 13 - PushData(ctx context.Context, peerID string, addresses []string, req maepproto.DataPushRequest, notification bool) (maepproto.DataPushResult, error) 14 - } 15 - 16 - type DeliveryAdapterOptions struct { 17 - Node DataPusher 18 - } 19 - 20 - type DeliveryAdapter struct { 21 - node DataPusher 22 - } 23 - 24 - func NewDeliveryAdapter(opts DeliveryAdapterOptions) (*DeliveryAdapter, error) { 25 - if opts.Node == nil { 26 - return nil, fmt.Errorf("node is required") 27 - } 28 - return &DeliveryAdapter{node: opts.Node}, nil 29 - } 30 - 31 - func (a *DeliveryAdapter) Deliver(ctx context.Context, msg busruntime.BusMessage) (bool, bool, error) { 32 - if a == nil || a.node == nil { 33 - return false, false, fmt.Errorf("maep delivery adapter is not initialized") 34 - } 35 - if ctx == nil { 36 - return false, false, fmt.Errorf("context is required") 37 - } 38 - if msg.Direction != busruntime.DirectionOutbound { 39 - return false, false, fmt.Errorf("direction must be outbound") 40 - } 41 - if msg.Channel != busruntime.ChannelMAEP { 42 - return false, false, fmt.Errorf("channel must be maep") 43 - } 44 - 45 - peerID, err := resolvePeerID(msg) 46 - if err != nil { 47 - return false, false, err 48 - } 49 - req := maepproto.DataPushRequest{ 50 - Topic: strings.TrimSpace(msg.Topic), 51 - ContentType: "application/json", 52 - PayloadBase64: strings.TrimSpace(msg.PayloadBase64), 53 - IdempotencyKey: strings.TrimSpace(msg.IdempotencyKey), 54 - } 55 - result, err := a.node.PushData(ctx, peerID, nil, req, false) 56 - if err != nil { 57 - return false, false, err 58 - } 59 - return result.Accepted, result.Deduped, nil 60 - }
-75
internal/bus/adapters/maep/delivery_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "testing" 6 - "time" 7 - 8 - busruntime "github.com/quailyquaily/mistermorph/internal/bus" 9 - maepproto "github.com/quailyquaily/mistermorph/maep" 10 - ) 11 - 12 - type mockDataPusher struct { 13 - peerID string 14 - req maepproto.DataPushRequest 15 - calls int 16 - } 17 - 18 - func (m *mockDataPusher) PushData(ctx context.Context, peerID string, addresses []string, req maepproto.DataPushRequest, notification bool) (maepproto.DataPushResult, error) { 19 - m.peerID = peerID 20 - m.req = req 21 - m.calls++ 22 - return maepproto.DataPushResult{Accepted: true, Deduped: false}, nil 23 - } 24 - 25 - func TestDeliveryAdapterDeliver(t *testing.T) { 26 - t.Parallel() 27 - 28 - mock := &mockDataPusher{} 29 - adapter, err := NewDeliveryAdapter(DeliveryAdapterOptions{Node: mock}) 30 - if err != nil { 31 - t.Fatalf("NewDeliveryAdapter() error = %v", err) 32 - } 33 - payloadBase64, err := busruntime.EncodeMessageEnvelope(busruntime.TopicDMReplyV1, busruntime.MessageEnvelope{ 34 - MessageID: "msg_3001", 35 - Text: "hi", 36 - SentAt: "2026-02-08T00:00:00Z", 37 - SessionID: "0194e9d5-2f8f-7000-8000-000000000001", 38 - }) 39 - if err != nil { 40 - t.Fatalf("EncodeMessageEnvelope() error = %v", err) 41 - } 42 - msg := busruntime.BusMessage{ 43 - Direction: busruntime.DirectionOutbound, 44 - Channel: busruntime.ChannelMAEP, 45 - Topic: busruntime.TopicDMReplyV1, 46 - ConversationKey: "maep:12D3KooWpeerC", 47 - ParticipantKey: "12D3KooWpeerC", 48 - IdempotencyKey: "msg:msg_3001", 49 - CorrelationID: "corr_2", 50 - PayloadBase64: payloadBase64, 51 - CreatedAt: time.Now().UTC(), 52 - } 53 - accepted, deduped, err := adapter.Deliver(context.Background(), msg) 54 - if err != nil { 55 - t.Fatalf("Deliver() error = %v", err) 56 - } 57 - if !accepted { 58 - t.Fatalf("accepted mismatch: got %v want true", accepted) 59 - } 60 - if deduped { 61 - t.Fatalf("deduped mismatch: got %v want false", deduped) 62 - } 63 - if mock.calls != 1 { 64 - t.Fatalf("PushData() calls mismatch: got %d want 1", mock.calls) 65 - } 66 - if mock.peerID != msg.ParticipantKey { 67 - t.Fatalf("peer_id mismatch: got %q want %q", mock.peerID, msg.ParticipantKey) 68 - } 69 - if mock.req.Topic != msg.Topic { 70 - t.Fatalf("topic mismatch: got %q want %q", mock.req.Topic, msg.Topic) 71 - } 72 - if mock.req.IdempotencyKey != msg.IdempotencyKey { 73 - t.Fatalf("idempotency_key mismatch: got %q want %q", mock.req.IdempotencyKey, msg.IdempotencyKey) 74 - } 75 - }
-149
internal/bus/adapters/maep/inbound.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "encoding/base64" 6 - "fmt" 7 - "strings" 8 - "time" 9 - 10 - busruntime "github.com/quailyquaily/mistermorph/internal/bus" 11 - baseadapters "github.com/quailyquaily/mistermorph/internal/bus/adapters" 12 - maepproto "github.com/quailyquaily/mistermorph/maep" 13 - ) 14 - 15 - type InboundAdapterOptions struct { 16 - Bus *busruntime.Inproc 17 - Store baseadapters.InboundStore 18 - Now func() time.Time 19 - } 20 - 21 - type InboundAdapter struct { 22 - flow *baseadapters.InboundFlow 23 - } 24 - 25 - func NewInboundAdapter(opts InboundAdapterOptions) (*InboundAdapter, error) { 26 - flow, err := baseadapters.NewInboundFlow(baseadapters.InboundFlowOptions{ 27 - Bus: opts.Bus, 28 - Store: opts.Store, 29 - Channel: string(busruntime.ChannelMAEP), 30 - Now: opts.Now, 31 - }) 32 - if err != nil { 33 - return nil, err 34 - } 35 - return &InboundAdapter{flow: flow}, nil 36 - } 37 - 38 - func (a *InboundAdapter) HandleDataPush(ctx context.Context, event maepproto.DataPushEvent) (bool, error) { 39 - if a == nil || a.flow == nil { 40 - return false, fmt.Errorf("maep inbound adapter is not initialized") 41 - } 42 - if ctx == nil { 43 - return false, fmt.Errorf("context is required") 44 - } 45 - 46 - fromPeerID := strings.TrimSpace(event.FromPeerID) 47 - if fromPeerID == "" { 48 - return false, fmt.Errorf("from_peer_id is required") 49 - } 50 - topic := strings.TrimSpace(event.Topic) 51 - if topic == "" { 52 - return false, fmt.Errorf("topic is required") 53 - } 54 - idempotencyKey := strings.TrimSpace(event.IdempotencyKey) 55 - if idempotencyKey == "" { 56 - return false, fmt.Errorf("idempotency_key is required") 57 - } 58 - payloadBase64 := strings.TrimSpace(event.PayloadBase64) 59 - if payloadBase64 == "" { 60 - return false, fmt.Errorf("payload_base64 is required") 61 - } 62 - receivedAt := event.ReceivedAt.UTC() 63 - if receivedAt.IsZero() { 64 - return false, fmt.Errorf("received_at is required") 65 - } 66 - 67 - conversationKey, err := busruntime.BuildMAEPPeerConversationKey(fromPeerID) 68 - if err != nil { 69 - return false, err 70 - } 71 - msg := busruntime.BusMessage{ 72 - Direction: busruntime.DirectionInbound, 73 - Channel: busruntime.ChannelMAEP, 74 - Topic: topic, 75 - ConversationKey: conversationKey, 76 - ParticipantKey: fromPeerID, 77 - IdempotencyKey: idempotencyKey, 78 - CorrelationID: "maep:" + idempotencyKey, 79 - PayloadBase64: payloadBase64, 80 - CreatedAt: receivedAt, 81 - Extensions: busruntime.MessageExtensions{ 82 - ReplyTo: strings.TrimSpace(event.ReplyTo), 83 - SessionID: strings.TrimSpace(event.SessionID), 84 - }, 85 - } 86 - platformMessageID := fmt.Sprintf("%s:%s:%s", fromPeerID, topic, idempotencyKey) 87 - return a.flow.PublishValidatedInbound(ctx, platformMessageID, msg) 88 - } 89 - 90 - func EventFromBusMessage(msg busruntime.BusMessage) (maepproto.DataPushEvent, error) { 91 - if msg.Direction != busruntime.DirectionInbound { 92 - return maepproto.DataPushEvent{}, fmt.Errorf("direction must be inbound") 93 - } 94 - if msg.Channel != busruntime.ChannelMAEP { 95 - return maepproto.DataPushEvent{}, fmt.Errorf("channel must be maep") 96 - } 97 - peerID, err := resolvePeerID(msg) 98 - if err != nil { 99 - return maepproto.DataPushEvent{}, err 100 - } 101 - payloadBytes, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(msg.PayloadBase64)) 102 - if err != nil { 103 - return maepproto.DataPushEvent{}, fmt.Errorf("payload_base64 decode failed: %w", err) 104 - } 105 - env, err := msg.Envelope() 106 - if err != nil { 107 - return maepproto.DataPushEvent{}, err 108 - } 109 - sessionID := strings.TrimSpace(msg.Extensions.SessionID) 110 - if sessionID == "" { 111 - sessionID = strings.TrimSpace(env.SessionID) 112 - } 113 - replyTo := strings.TrimSpace(msg.Extensions.ReplyTo) 114 - if replyTo == "" { 115 - replyTo = strings.TrimSpace(env.ReplyTo) 116 - } 117 - receivedAt := msg.CreatedAt.UTC() 118 - if receivedAt.IsZero() { 119 - return maepproto.DataPushEvent{}, fmt.Errorf("created_at is required") 120 - } 121 - return maepproto.DataPushEvent{ 122 - FromPeerID: peerID, 123 - Topic: msg.Topic, 124 - ContentType: "application/json", 125 - PayloadBase64: msg.PayloadBase64, 126 - PayloadBytes: payloadBytes, 127 - IdempotencyKey: msg.IdempotencyKey, 128 - SessionID: sessionID, 129 - ReplyTo: replyTo, 130 - ReceivedAt: receivedAt, 131 - Deduped: false, 132 - }, nil 133 - } 134 - 135 - func resolvePeerID(msg busruntime.BusMessage) (string, error) { 136 - peerID := strings.TrimSpace(msg.ParticipantKey) 137 - if peerID != "" { 138 - return peerID, nil 139 - } 140 - const prefix = "maep:" 141 - if !strings.HasPrefix(msg.ConversationKey, prefix) { 142 - return "", fmt.Errorf("participant_key is required for maep message") 143 - } 144 - peerID = strings.TrimSpace(strings.TrimPrefix(msg.ConversationKey, prefix)) 145 - if peerID == "" { 146 - return "", fmt.Errorf("participant_key is required for maep message") 147 - } 148 - return peerID, nil 149 - }
-121
internal/bus/adapters/maep/inbound_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "io" 6 - "log/slog" 7 - "testing" 8 - "time" 9 - 10 - "github.com/quailyquaily/mistermorph/contacts" 11 - busruntime "github.com/quailyquaily/mistermorph/internal/bus" 12 - maepproto "github.com/quailyquaily/mistermorph/maep" 13 - ) 14 - 15 - func TestInboundAdapterHandleDataPush(t *testing.T) { 16 - t.Parallel() 17 - 18 - logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})) 19 - bus, err := busruntime.NewInproc(busruntime.InprocOptions{MaxInFlight: 4, Logger: logger}) 20 - if err != nil { 21 - t.Fatalf("NewInproc() error = %v", err) 22 - } 23 - defer bus.Close() 24 - 25 - store := contacts.NewFileStore(t.TempDir()) 26 - adapter, err := NewInboundAdapter(InboundAdapterOptions{ 27 - Bus: bus, 28 - Store: store, 29 - }) 30 - if err != nil { 31 - t.Fatalf("NewInboundAdapter() error = %v", err) 32 - } 33 - 34 - delivered := make(chan busruntime.BusMessage, 1) 35 - if err := bus.Subscribe(busruntime.TopicChatMessage, func(ctx context.Context, msg busruntime.BusMessage) error { 36 - delivered <- msg 37 - return nil 38 - }); err != nil { 39 - t.Fatalf("Subscribe() error = %v", err) 40 - } 41 - 42 - payloadBase64, err := busruntime.EncodeMessageEnvelope(busruntime.TopicChatMessage, busruntime.MessageEnvelope{ 43 - MessageID: "msg_1001", 44 - Text: "hello", 45 - SentAt: "2026-02-08T00:00:00Z", 46 - SessionID: "0194e9d5-2f8f-7000-8000-000000000001", 47 - }) 48 - if err != nil { 49 - t.Fatalf("EncodeMessageEnvelope() error = %v", err) 50 - } 51 - event := maepproto.DataPushEvent{ 52 - FromPeerID: "12D3KooWpeerA", 53 - Topic: busruntime.TopicChatMessage, 54 - ContentType: "application/json", 55 - PayloadBase64: payloadBase64, 56 - IdempotencyKey: "msg:msg_1001", 57 - ReceivedAt: time.Now().UTC(), 58 - } 59 - accepted, err := adapter.HandleDataPush(context.Background(), event) 60 - if err != nil { 61 - t.Fatalf("HandleDataPush() error = %v", err) 62 - } 63 - if !accepted { 64 - t.Fatalf("HandleDataPush() accepted=false, want true") 65 - } 66 - 67 - select { 68 - case msg := <-delivered: 69 - if msg.Channel != busruntime.ChannelMAEP { 70 - t.Fatalf("channel mismatch: got %s want %s", msg.Channel, busruntime.ChannelMAEP) 71 - } 72 - if msg.ParticipantKey != event.FromPeerID { 73 - t.Fatalf("participant_key mismatch: got %q want %q", msg.ParticipantKey, event.FromPeerID) 74 - } 75 - case <-time.After(2 * time.Second): 76 - t.Fatalf("message not delivered") 77 - } 78 - 79 - accepted, err = adapter.HandleDataPush(context.Background(), event) 80 - if err != nil { 81 - t.Fatalf("HandleDataPush(second) error = %v", err) 82 - } 83 - if accepted { 84 - t.Fatalf("HandleDataPush(second) accepted=true, want false") 85 - } 86 - } 87 - 88 - func TestEventFromBusMessage(t *testing.T) { 89 - t.Parallel() 90 - 91 - payloadBase64, err := busruntime.EncodeMessageEnvelope(busruntime.TopicDMReplyV1, busruntime.MessageEnvelope{ 92 - MessageID: "msg_2001", 93 - Text: "reply", 94 - SentAt: "2026-02-08T00:00:00Z", 95 - SessionID: "0194e9d5-2f8f-7000-8000-000000000001", 96 - }) 97 - if err != nil { 98 - t.Fatalf("EncodeMessageEnvelope() error = %v", err) 99 - } 100 - msg := busruntime.BusMessage{ 101 - Direction: busruntime.DirectionInbound, 102 - Channel: busruntime.ChannelMAEP, 103 - Topic: busruntime.TopicDMReplyV1, 104 - ConversationKey: "maep:12D3KooWpeerB", 105 - ParticipantKey: "12D3KooWpeerB", 106 - IdempotencyKey: "msg:msg_2001", 107 - CorrelationID: "corr_1", 108 - PayloadBase64: payloadBase64, 109 - CreatedAt: time.Now().UTC(), 110 - } 111 - event, err := EventFromBusMessage(msg) 112 - if err != nil { 113 - t.Fatalf("EventFromBusMessage() error = %v", err) 114 - } 115 - if event.FromPeerID != msg.ParticipantKey { 116 - t.Fatalf("from_peer_id mismatch: got %q want %q", event.FromPeerID, msg.ParticipantKey) 117 - } 118 - if event.Topic != msg.Topic { 119 - t.Fatalf("topic mismatch: got %q want %q", event.Topic, msg.Topic) 120 - } 121 - }
+1 -7
internal/bus/conversation_key.go
··· 31 31 return BuildConversationKey(ChannelDiscord, channelID) 32 32 } 33 33 34 - func BuildMAEPPeerConversationKey(peerID string) (string, error) { 35 - return BuildConversationKey(ChannelMAEP, peerID) 36 - } 37 - 38 34 func isValidChannel(channel Channel) bool { 39 35 switch channel { 40 - case ChannelTelegram, ChannelSlack, ChannelDiscord, ChannelMAEP: 36 + case ChannelTelegram, ChannelSlack, ChannelDiscord: 41 37 return true 42 38 default: 43 39 return false ··· 48 44 switch channel { 49 45 case ChannelTelegram: 50 46 return "tg" 51 - case ChannelMAEP: 52 - return "maep" 53 47 case ChannelSlack: 54 48 return "slack" 55 49 case ChannelDiscord:
+1 -2
internal/bus/message.go
··· 20 20 ChannelTelegram Channel = Channel(channels.Telegram) 21 21 ChannelSlack Channel = Channel(channels.Slack) 22 22 ChannelDiscord Channel = Channel(channels.Discord) 23 - ChannelMAEP Channel = Channel(channels.MAEP) 24 23 ) 25 24 26 25 type MessageExtensions struct { ··· 72 71 73 72 if m.Channel != "" { 74 73 switch m.Channel { 75 - case ChannelTelegram, ChannelSlack, ChannelDiscord, ChannelMAEP: 74 + case ChannelTelegram, ChannelSlack, ChannelDiscord: 76 75 default: 77 76 return fmt.Errorf("channel is invalid") 78 77 }
-16
internal/channelopts/options.go
··· 26 26 DefaultGroupTriggerMode string 27 27 DefaultAddressingConfidenceThreshold float64 28 28 DefaultAddressingInterjectThreshold float64 29 - MAEPListenAddrs []string 30 29 PollTimeout time.Duration 31 30 TaskTimeout time.Duration 32 31 GlobalTaskTimeout time.Duration ··· 50 49 MemoryInjectionEnabled bool 51 50 MemoryInjectionMaxItems int 52 51 SecretsRequireSkillProfiles bool 53 - ContactsProactiveMaxTurnsPerSession int 54 - ContactsProactiveSessionCooldown time.Duration 55 52 } 56 53 57 54 type TelegramInput struct { ··· 60 57 GroupTriggerMode string 61 58 AddressingConfidenceThreshold float64 62 59 AddressingInterjectThreshold float64 63 - WithMAEP bool 64 - MAEPListenAddrs []string 65 60 PollTimeout time.Duration 66 61 TaskTimeout time.Duration 67 62 MaxConcurrency int ··· 80 75 DefaultGroupTriggerMode: strings.TrimSpace(r.GetString("telegram.group_trigger_mode")), 81 76 DefaultAddressingConfidenceThreshold: r.GetFloat64("telegram.addressing_confidence_threshold"), 82 77 DefaultAddressingInterjectThreshold: r.GetFloat64("telegram.addressing_interject_threshold"), 83 - MAEPListenAddrs: append([]string(nil), r.GetStringSlice("maep.listen_addrs")...), 84 78 PollTimeout: r.GetDuration("telegram.poll_timeout"), 85 79 TaskTimeout: r.GetDuration("telegram.task_timeout"), 86 80 GlobalTaskTimeout: r.GetDuration("timeout"), ··· 104 98 MemoryInjectionEnabled: r.GetBool("memory.injection.enabled"), 105 99 MemoryInjectionMaxItems: r.GetInt("memory.injection.max_items"), 106 100 SecretsRequireSkillProfiles: r.GetBool("secrets.require_skill_profiles"), 107 - ContactsProactiveMaxTurnsPerSession: r.GetInt("contacts.proactive.max_turns_per_session"), 108 - ContactsProactiveSessionCooldown: r.GetDuration("contacts.proactive.session_cooldown"), 109 101 } 110 102 } 111 103 ··· 135 127 if addressingInterjectThreshold <= 0 { 136 128 addressingInterjectThreshold = cfg.DefaultAddressingInterjectThreshold 137 129 } 138 - maepListenAddrs := append([]string(nil), in.MAEPListenAddrs...) 139 - if len(maepListenAddrs) == 0 { 140 - maepListenAddrs = append([]string(nil), cfg.MAEPListenAddrs...) 141 - } 142 130 pollTimeout := in.PollTimeout 143 131 if pollTimeout <= 0 { 144 132 pollTimeout = cfg.PollTimeout ··· 166 154 GroupTriggerMode: groupTriggerMode, 167 155 AddressingConfidenceThreshold: addressingConfidenceThreshold, 168 156 AddressingInterjectThreshold: addressingInterjectThreshold, 169 - WithMAEP: in.WithMAEP, 170 - MAEPListenAddrs: maepListenAddrs, 171 157 PollTimeout: pollTimeout, 172 158 TaskTimeout: taskTimeout, 173 159 MaxConcurrency: maxConcurrency, ··· 190 176 MemoryInjectionEnabled: cfg.MemoryInjectionEnabled, 191 177 MemoryInjectionMaxItems: cfg.MemoryInjectionMaxItems, 192 178 SecretsRequireSkillProfiles: cfg.SecretsRequireSkillProfiles, 193 - MAEPMaxTurnsPerSession: cfg.ContactsProactiveMaxTurnsPerSession, 194 - MAEPSessionCooldown: cfg.ContactsProactiveSessionCooldown, 195 179 Hooks: in.Hooks, 196 180 InspectPrompt: in.InspectPrompt, 197 181 InspectRequest: in.InspectRequest,
+1 -1
internal/channelruntime/slack/runtime.go
··· 491 491 if msg.Channel != busruntime.ChannelSlack { 492 492 return fmt.Errorf("unsupported inbound channel: %s", msg.Channel) 493 493 } 494 - if err := contactsSvc.ObserveInboundBusMessage(context.Background(), msg, nil, time.Now().UTC()); err != nil { 494 + if err := contactsSvc.ObserveInboundBusMessage(context.Background(), msg, time.Now().UTC()); err != nil { 495 495 logger.Warn("contacts_observe_bus_error", "channel", msg.Channel, "idempotency_key", msg.IdempotencyKey, "error", err.Error()) 496 496 } 497 497 if enqueueSlackInbound == nil {
-153
internal/channelruntime/telegram/bus_cross_channel_test.go
··· 1 - package telegram 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "io" 7 - "log/slog" 8 - "path/filepath" 9 - "strings" 10 - "testing" 11 - "time" 12 - 13 - "github.com/quailyquaily/mistermorph/contacts" 14 - busruntime "github.com/quailyquaily/mistermorph/internal/bus" 15 - maepbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/maep" 16 - telegrambus "github.com/quailyquaily/mistermorph/internal/bus/adapters/telegram" 17 - "github.com/quailyquaily/mistermorph/maep" 18 - ) 19 - 20 - func TestBusCrossChannelTelegramInboundToMAEPOutbound(t *testing.T) { 21 - ctx := context.Background() 22 - 23 - store := contacts.NewFileStore(filepath.Join(t.TempDir(), "contacts")) 24 - if err := store.Ensure(ctx); err != nil { 25 - t.Fatalf("Ensure() error = %v", err) 26 - } 27 - 28 - logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})) 29 - inprocBus, err := busruntime.NewInproc(busruntime.InprocOptions{ 30 - MaxInFlight: 8, 31 - Logger: logger, 32 - }) 33 - if err != nil { 34 - t.Fatalf("NewInproc() error = %v", err) 35 - } 36 - defer inprocBus.Close() 37 - 38 - telegramInbound, err := telegrambus.NewInboundAdapter(telegrambus.InboundAdapterOptions{ 39 - Bus: inprocBus, 40 - Store: store, 41 - }) 42 - if err != nil { 43 - t.Fatalf("NewInboundAdapter(telegram) error = %v", err) 44 - } 45 - 46 - mockNode := &crossChannelMockNode{ 47 - result: maep.DataPushResult{Accepted: true}, 48 - reqCh: make(chan maep.DataPushRequest, 1), 49 - } 50 - maepDelivery, err := maepbus.NewDeliveryAdapter(maepbus.DeliveryAdapterOptions{ 51 - Node: mockNode, 52 - }) 53 - if err != nil { 54 - t.Fatalf("NewDeliveryAdapter(maep) error = %v", err) 55 - } 56 - 57 - const peerID = "12D3KooWCrossPeer" 58 - busHandler := func(handlerCtx context.Context, msg busruntime.BusMessage) error { 59 - switch msg.Direction { 60 - case busruntime.DirectionInbound: 61 - if msg.Channel != busruntime.ChannelTelegram { 62 - return fmt.Errorf("unsupported inbound channel: %s", msg.Channel) 63 - } 64 - inbound, err := telegrambus.InboundMessageFromBusMessage(msg) 65 - if err != nil { 66 - return err 67 - } 68 - sessionID := strings.TrimSpace(msg.Extensions.SessionID) 69 - if sessionID == "" { 70 - return fmt.Errorf("session_id is required") 71 - } 72 - _, err = publishMAEPBusOutbound( 73 - handlerCtx, 74 - inprocBus, 75 - peerID, 76 - busruntime.TopicDMReplyV1, 77 - "ack: "+inbound.Text, 78 - sessionID, 79 - msg.Extensions.PlatformMessageID, 80 - "test:cross_channel", 81 - ) 82 - return err 83 - case busruntime.DirectionOutbound: 84 - if msg.Channel != busruntime.ChannelMAEP { 85 - return fmt.Errorf("unsupported outbound channel: %s", msg.Channel) 86 - } 87 - _, _, err := maepDelivery.Deliver(handlerCtx, msg) 88 - return err 89 - default: 90 - return fmt.Errorf("unsupported direction: %s", msg.Direction) 91 - } 92 - } 93 - for _, topic := range busruntime.AllTopics() { 94 - if err := inprocBus.Subscribe(topic, busHandler); err != nil { 95 - t.Fatalf("Subscribe(%s) error = %v", topic, err) 96 - } 97 - } 98 - 99 - accepted, err := telegramInbound.HandleInboundMessage(ctx, telegrambus.InboundMessage{ 100 - ChatID: 10001, 101 - MessageID: 20002, 102 - ChatType: "private", 103 - Text: "hello from telegram", 104 - }) 105 - if err != nil { 106 - t.Fatalf("HandleInboundMessage() error = %v", err) 107 - } 108 - if !accepted { 109 - t.Fatalf("HandleInboundMessage() accepted=false, want true") 110 - } 111 - 112 - req, got := mockNode.waitRequest(2 * time.Second) 113 - if !got { 114 - t.Fatalf("expected MAEP outbound delivery") 115 - } 116 - if req.Topic != busruntime.TopicDMReplyV1 { 117 - t.Fatalf("topic mismatch: got %q want %q", req.Topic, busruntime.TopicDMReplyV1) 118 - } 119 - env, err := busruntime.DecodeMessageEnvelope(req.Topic, req.PayloadBase64) 120 - if err != nil { 121 - t.Fatalf("DecodeMessageEnvelope() error = %v", err) 122 - } 123 - if env.Text != "ack: hello from telegram" { 124 - t.Fatalf("reply text mismatch: got %q want %q", env.Text, "ack: hello from telegram") 125 - } 126 - if strings.TrimSpace(env.SessionID) == "" { 127 - t.Fatalf("session_id mismatch: expected non-empty") 128 - } 129 - } 130 - 131 - type crossChannelMockNode struct { 132 - result maep.DataPushResult 133 - err error 134 - 135 - reqCh chan maep.DataPushRequest 136 - } 137 - 138 - func (m *crossChannelMockNode) PushData(ctx context.Context, peerID string, addresses []string, req maep.DataPushRequest, notification bool) (maep.DataPushResult, error) { 139 - select { 140 - case m.reqCh <- req: 141 - default: 142 - } 143 - return m.result, m.err 144 - } 145 - 146 - func (m *crossChannelMockNode) waitRequest(timeout time.Duration) (maep.DataPushRequest, bool) { 147 - select { 148 - case req := <-m.reqCh: 149 - return req, true 150 - case <-time.After(timeout): 151 - return maep.DataPushRequest{}, false 152 - } 153 - }
-53
internal/channelruntime/telegram/bus_publish_test.go
··· 104 104 t.Fatalf("message not delivered") 105 105 } 106 106 } 107 - 108 - func TestPublishMAEPBusOutbound(t *testing.T) { 109 - t.Parallel() 110 - 111 - logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})) 112 - bus, err := busruntime.NewInproc(busruntime.InprocOptions{MaxInFlight: 4, Logger: logger}) 113 - if err != nil { 114 - t.Fatalf("NewInproc() error = %v", err) 115 - } 116 - defer bus.Close() 117 - 118 - got := make(chan busruntime.BusMessage, 1) 119 - if err := bus.Subscribe(busruntime.TopicDMReplyV1, func(ctx context.Context, msg busruntime.BusMessage) error { 120 - got <- msg 121 - return nil 122 - }); err != nil { 123 - t.Fatalf("Subscribe() error = %v", err) 124 - } 125 - 126 - sessionID := "0194e9d5-2f8f-7000-8000-000000000001" 127 - messageID, err := publishMAEPBusOutbound(context.Background(), bus, "12D3KooWpeerZ", busruntime.TopicDMReplyV1, "reply", sessionID, "", "corr:maep") 128 - if err != nil { 129 - t.Fatalf("publishMAEPBusOutbound() error = %v", err) 130 - } 131 - if messageID == "" { 132 - t.Fatalf("message_id should not be empty") 133 - } 134 - 135 - select { 136 - case msg := <-got: 137 - if msg.Direction != busruntime.DirectionOutbound { 138 - t.Fatalf("direction mismatch: got %s want %s", msg.Direction, busruntime.DirectionOutbound) 139 - } 140 - if msg.Channel != busruntime.ChannelMAEP { 141 - t.Fatalf("channel mismatch: got %s want %s", msg.Channel, busruntime.ChannelMAEP) 142 - } 143 - if msg.ParticipantKey != "12D3KooWpeerZ" { 144 - t.Fatalf("participant_key mismatch: got %q want %q", msg.ParticipantKey, "12D3KooWpeerZ") 145 - } 146 - env, err := msg.Envelope() 147 - if err != nil { 148 - t.Fatalf("Envelope() error = %v", err) 149 - } 150 - if env.Text != "reply" { 151 - t.Fatalf("envelope text mismatch: got %q want %q", env.Text, "reply") 152 - } 153 - if env.SessionID != sessionID { 154 - t.Fatalf("envelope session_id mismatch: got %q want %q", env.SessionID, sessionID) 155 - } 156 - case <-time.After(2 * time.Second): 157 - t.Fatalf("message not delivered") 158 - } 159 - }
-53
internal/channelruntime/telegram/maep_prompts.go
··· 1 - package telegram 2 - 3 - import ( 4 - _ "embed" 5 - "encoding/json" 6 - "text/template" 7 - 8 - "github.com/quailyquaily/mistermorph/internal/prompttmpl" 9 - "github.com/quailyquaily/mistermorph/llm" 10 - ) 11 - 12 - //go:embed prompts/maep_feedback_system.tmpl 13 - var maepFeedbackSystemPromptTemplateSource string 14 - 15 - //go:embed prompts/maep_feedback_user.tmpl 16 - var maepFeedbackUserPromptTemplateSource string 17 - 18 - var maepPromptTemplateFuncs = template.FuncMap{ 19 - "toJSON": func(v any) (string, error) { 20 - b, err := json.Marshal(v) 21 - if err != nil { 22 - return "", err 23 - } 24 - return string(b), nil 25 - }, 26 - } 27 - 28 - var maepFeedbackSystemPromptTemplate = prompttmpl.MustParse("telegram_maep_feedback_system", maepFeedbackSystemPromptTemplateSource, nil) 29 - var maepFeedbackUserPromptTemplate = prompttmpl.MustParse("telegram_maep_feedback_user", maepFeedbackUserPromptTemplateSource, maepPromptTemplateFuncs) 30 - 31 - type maepFeedbackUserPromptData struct { 32 - RecentTurns []llm.Message 33 - InboundText string 34 - AllowedNext []string 35 - SignalBounds string 36 - } 37 - 38 - func renderMAEPFeedbackPrompts(recentTurns []llm.Message, inboundText string) (string, string, error) { 39 - systemPrompt, err := prompttmpl.Render(maepFeedbackSystemPromptTemplate, struct{}{}) 40 - if err != nil { 41 - return "", "", err 42 - } 43 - userPrompt, err := prompttmpl.Render(maepFeedbackUserPromptTemplate, maepFeedbackUserPromptData{ 44 - RecentTurns: recentTurns, 45 - InboundText: inboundText, 46 - AllowedNext: []string{"continue", "wrap_up", "switch_topic"}, 47 - SignalBounds: "[0,1]", 48 - }) 49 - if err != nil { 50 - return "", "", err 51 - } 52 - return systemPrompt, userPrompt, nil 53 - }
-47
internal/channelruntime/telegram/maep_prompts_test.go
··· 1 - package telegram 2 - 3 - import ( 4 - "encoding/json" 5 - "strings" 6 - "testing" 7 - 8 - "github.com/quailyquaily/mistermorph/llm" 9 - ) 10 - 11 - func TestRenderMAEPFeedbackPrompts(t *testing.T) { 12 - recent := []llm.Message{ 13 - {Role: "user", Content: "hello"}, 14 - {Role: "assistant", Content: "hi"}, 15 - } 16 - inbound := "sounds good" 17 - 18 - sys, user, err := renderMAEPFeedbackPrompts(recent, inbound) 19 - if err != nil { 20 - t.Fatalf("renderMAEPFeedbackPrompts() error = %v", err) 21 - } 22 - if !strings.Contains(sys, "Classify conversational feedback") { 23 - t.Fatalf("unexpected system prompt: %q", sys) 24 - } 25 - 26 - var payload struct { 27 - RecentTurns []llm.Message `json:"recent_turns"` 28 - InboundText string `json:"inbound_text"` 29 - AllowedNext []string `json:"allowed_next"` 30 - SignalBounds string `json:"signal_bounds"` 31 - } 32 - if err := json.Unmarshal([]byte(user), &payload); err != nil { 33 - t.Fatalf("user prompt is not valid json: %v", err) 34 - } 35 - if payload.InboundText != inbound { 36 - t.Fatalf("inbound_text = %q, want %q", payload.InboundText, inbound) 37 - } 38 - if len(payload.RecentTurns) != len(recent) { 39 - t.Fatalf("recent_turns len = %d, want %d", len(payload.RecentTurns), len(recent)) 40 - } 41 - if len(payload.AllowedNext) == 0 { 42 - t.Fatalf("allowed_next is empty") 43 - } 44 - if payload.SignalBounds != "[0,1]" { 45 - t.Fatalf("signal_bounds = %q, want %q", payload.SignalBounds, "[0,1]") 46 - } 47 - }
-4
internal/channelruntime/telegram/prompts/maep_feedback_system.tmpl
··· 1 - Classify conversational feedback into numeric signals. 2 - Return JSON only with schema: 3 - {"signal_positive":0..1,"signal_negative":0..1,"signal_bored":0..1,"next_action":"continue|wrap_up|switch_topic","confidence":0..1}. 4 - Use wrap_up only when the user shows clear stop/low-interest intent.
-6
internal/channelruntime/telegram/prompts/maep_feedback_user.tmpl
··· 1 - { 2 - "recent_turns": {{toJSON .RecentTurns}}, 3 - "inbound_text": {{toJSON .InboundText}}, 4 - "allowed_next": {{toJSON .AllowedNext}}, 5 - "signal_bounds": {{toJSON .SignalBounds}} 6 - }
-4
internal/channelruntime/telegram/run.go
··· 12 12 GroupTriggerMode string 13 13 AddressingConfidenceThreshold float64 14 14 AddressingInterjectThreshold float64 15 - WithMAEP bool 16 - MAEPListenAddrs []string 17 15 PollTimeout time.Duration 18 16 TaskTimeout time.Duration 19 17 MaxConcurrency int ··· 36 34 MemoryInjectionEnabled bool 37 35 MemoryInjectionMaxItems int 38 36 SecretsRequireSkillProfiles bool 39 - MAEPMaxTurnsPerSession int 40 - MAEPSessionCooldown time.Duration 41 37 Hooks Hooks 42 38 InspectPrompt bool 43 39 InspectRequest bool
+2 -294
internal/channelruntime/telegram/runtime.go
··· 16 16 "github.com/quailyquaily/mistermorph/contacts" 17 17 "github.com/quailyquaily/mistermorph/guard" 18 18 busruntime "github.com/quailyquaily/mistermorph/internal/bus" 19 - maepbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/maep" 20 19 telegrambus "github.com/quailyquaily/mistermorph/internal/bus/adapters/telegram" 21 20 runtimeworker "github.com/quailyquaily/mistermorph/internal/channelruntime/worker" 22 21 "github.com/quailyquaily/mistermorph/internal/chathistory" ··· 24 23 "github.com/quailyquaily/mistermorph/internal/heartbeatutil" 25 24 "github.com/quailyquaily/mistermorph/internal/llmconfig" 26 25 "github.com/quailyquaily/mistermorph/internal/llminspect" 27 - "github.com/quailyquaily/mistermorph/internal/maepruntime" 28 26 "github.com/quailyquaily/mistermorph/internal/statepaths" 29 27 "github.com/quailyquaily/mistermorph/internal/telegramutil" 30 28 "github.com/quailyquaily/mistermorph/internal/toolsutil" 31 - "github.com/quailyquaily/mistermorph/llm" 32 - "github.com/quailyquaily/mistermorph/maep" 33 29 "github.com/quailyquaily/mistermorph/memory" 34 30 telegramtools "github.com/quailyquaily/mistermorph/tools/telegram" 35 31 ) ··· 58 54 Version uint64 59 55 } 60 56 61 - type maepSessionState struct { 62 - TurnCount int 63 - CooldownUntil time.Time 64 - UpdatedAt time.Time 65 - InterestLevel float64 66 - LowInterestRounds int 67 - PreferenceSynced bool 68 - } 69 - 70 - const ( 71 - defaultMAEPMaxTurnsPerSession = 6 72 - defaultMAEPSessionCooldown = 72 * time.Hour 73 - defaultMAEPInterestLevel = 0.60 74 - maepInterestStopThreshold = 0.30 75 - maepInterestLowRoundsLimit = 2 76 - maepWrapUpConfidenceThreshold = 0.70 77 - maepFeedbackNegativeThreshold = 0.55 78 - maepFeedbackPositiveThreshold = 0.60 79 - ) 80 - 81 - type maepFeedbackClassification struct { 82 - SignalPositive float64 `json:"signal_positive"` 83 - SignalNegative float64 `json:"signal_negative"` 84 - SignalBored float64 `json:"signal_bored"` 85 - NextAction string `json:"next_action"` 86 - Confidence float64 `json:"confidence"` 87 - } 88 - 89 57 func shouldRunInitFlow(initRequired bool, normalizedCmd string) bool { 90 58 if !initRequired { 91 59 return false ··· 135 103 contactsStore := contacts.NewFileStore(statepaths.ContactsDir()) 136 104 contactsSvc := contacts.NewService(contactsStore) 137 105 138 - withMAEP := opts.WithMAEP 139 - var maepNode *maep.Node 140 - var maepInboundAdapter *maepbus.InboundAdapter 141 106 var telegramInboundAdapter *telegrambus.InboundAdapter 142 - var maepDeliveryAdapter *maepbus.DeliveryAdapter 143 107 var telegramDeliveryAdapter *telegrambus.DeliveryAdapter 144 - maepEventCh := make(chan maep.DataPushEvent, 64) 145 108 var enqueueTelegramInbound func(context.Context, busruntime.BusMessage) error 146 109 telegramInboundAdapter, err = telegrambus.NewInboundAdapter(telegrambus.InboundAdapterOptions{ 147 110 Bus: inprocBus, ··· 150 113 if err != nil { 151 114 return err 152 115 } 153 - maepSvc := maep.NewService(maep.NewFileStore(statepaths.MAEPDir())) 154 116 155 117 busHandler := func(ctx context.Context, msg busruntime.BusMessage) error { 156 118 switch msg.Direction { 157 119 case busruntime.DirectionInbound: 158 - if msg.Channel == busruntime.ChannelTelegram || msg.Channel == busruntime.ChannelMAEP { 159 - if err := contactsSvc.ObserveInboundBusMessage(context.Background(), msg, maepSvc, time.Now().UTC()); err != nil { 120 + if msg.Channel == busruntime.ChannelTelegram { 121 + if err := contactsSvc.ObserveInboundBusMessage(context.Background(), msg, time.Now().UTC()); err != nil { 160 122 logger.Warn("contacts_observe_bus_error", "channel", msg.Channel, "idempotency_key", msg.IdempotencyKey, "error", err.Error()) 161 123 } 162 124 } ··· 166 128 return fmt.Errorf("telegram inbound handler is not initialized") 167 129 } 168 130 return enqueueTelegramInbound(ctx, msg) 169 - case busruntime.ChannelMAEP: 170 - event, err := maepbus.EventFromBusMessage(msg) 171 - if err != nil { 172 - return err 173 - } 174 - select { 175 - case maepEventCh <- event: 176 - logger.Debug("telegram_bus_inbound_forwarded", "channel", msg.Channel, "topic", msg.Topic, "idempotency_key", msg.IdempotencyKey) 177 - return nil 178 - default: 179 - return fmt.Errorf("maep inbound queue is full") 180 - } 181 131 default: 182 132 return fmt.Errorf("unsupported inbound channel: %s", msg.Channel) 183 133 } ··· 208 158 callOutboundHook(ctx, logger, hooks, event) 209 159 } 210 160 return nil 211 - case busruntime.ChannelMAEP: 212 - if maepDeliveryAdapter == nil { 213 - return fmt.Errorf("maep delivery adapter is not initialized") 214 - } 215 - _, _, err := maepDeliveryAdapter.Deliver(ctx, msg) 216 - return err 217 161 default: 218 162 return fmt.Errorf("unsupported outbound channel: %s", msg.Channel) 219 163 } ··· 227 171 } 228 172 } 229 173 230 - if withMAEP { 231 - maepInboundAdapter, err = maepbus.NewInboundAdapter(maepbus.InboundAdapterOptions{ 232 - Bus: inprocBus, 233 - Store: contactsStore, 234 - }) 235 - if err != nil { 236 - return err 237 - } 238 - maepListenAddrs := append([]string(nil), opts.MAEPListenAddrs...) 239 - maepNode, err = maepruntime.Start(pollCtx, maepruntime.StartOptions{ 240 - ListenAddrs: maepListenAddrs, 241 - Logger: logger, 242 - OnDataPush: func(event maep.DataPushEvent) { 243 - accepted, publishErr := maepInboundAdapter.HandleDataPush(context.Background(), event) 244 - if publishErr != nil { 245 - logger.Warn("telegram_maep_bus_publish_error", "from_peer_id", event.FromPeerID, "topic", event.Topic, "bus_error_code", busErrorCodeString(publishErr), "error", publishErr.Error()) 246 - return 247 - } 248 - if !accepted { 249 - logger.Debug("telegram_maep_bus_deduped", "from_peer_id", event.FromPeerID, "topic", event.Topic, "idempotency_key", event.IdempotencyKey) 250 - } 251 - }, 252 - }) 253 - if err != nil { 254 - return fmt.Errorf("start embedded maep: %w", err) 255 - } 256 - maepDeliveryAdapter, err = maepbus.NewDeliveryAdapter(maepbus.DeliveryAdapterOptions{ 257 - Node: maepNode, 258 - }) 259 - if err != nil { 260 - return err 261 - } 262 - defer maepNode.Close() 263 - logger.Info("telegram_maep_ready", "peer_id", maepNode.PeerID(), "addresses", maepNode.AddrStrings()) 264 - } 265 - 266 174 requestTimeout := opts.RequestTimeout 267 175 client, err := llmClientFromConfig(d, llmconfig.ClientConfig{ 268 176 Provider: llmProviderFromDeps(d), ··· 358 266 } 359 267 } 360 268 361 - maepMaxTurnsPerSession := opts.MAEPMaxTurnsPerSession 362 - maepSessionCooldown := opts.MAEPSessionCooldown 363 - runtimeStore, err := maepruntime.NewStateStore(statepaths.MAEPDir()) 364 - if err != nil { 365 - return fmt.Errorf("init telegram runtime state store: %w", err) 366 - } 367 - runtimeSnapshot, runtimeStateFound, err := runtimeStore.Load() 368 - if err != nil { 369 - logger.Warn("telegram_runtime_state_load_error", "error", err.Error()) 370 - runtimeSnapshot = maepruntime.StateSnapshot{} 371 - runtimeStateFound = false 372 - } 373 - 374 269 httpClient := &http.Client{Timeout: 60 * time.Second} 375 270 api := newTelegramAPI(httpClient, baseURL, token) 376 271 telegramDeliveryAdapter, err = telegrambus.NewDeliveryAdapter(telegrambus.DeliveryAdapterOptions{ ··· 456 351 mu sync.Mutex 457 352 history = make(map[int64][]chathistory.ChatHistoryItem) 458 353 initSessions = make(map[int64]telegramInitSession) 459 - maepMu sync.Mutex 460 - maepHistory = make(map[string][]llm.Message) 461 - maepStickySkills = make(map[string][]string) 462 - maepSessions = make(map[string]maepSessionState) 463 - maepSessionDirty bool 464 - maepVersion uint64 465 354 stickySkillsByChat = make(map[int64][]string) 466 355 workers = make(map[int64]*telegramChatWorker) 467 356 lastActivity = make(map[int64]time.Time) ··· 475 364 heartbeatState = &heartbeatutil.State{} 476 365 offset int64 477 366 ) 478 - if runtimeStateFound { 479 - restoredOffset, ok := runtimeSnapshot.ChannelOffsets[maepruntime.ChannelTelegram] 480 - if !ok { 481 - logger.Warn("telegram_runtime_state_missing_offset", "channel", maepruntime.ChannelTelegram) 482 - } else { 483 - offset = restoredOffset 484 - maepSessions = restoreMAEPSessionStates(runtimeSnapshot.SessionStates) 485 - logger.Info("telegram_runtime_state_loaded", "offset", offset, "session_states", len(maepSessions)) 486 - } 487 - } 488 367 initRequired := false 489 368 if _, err := loadInitProfileDraft(); err == nil { 490 369 initRequired = true ··· 975 854 }() 976 855 } 977 856 978 - if withMAEP && maepNode != nil { 979 - go func() { 980 - for { 981 - select { 982 - case <-pollCtx.Done(): 983 - return 984 - case event := <-maepEventCh: 985 - if event.Deduped { 986 - continue 987 - } 988 - peerID := strings.TrimSpace(event.FromPeerID) 989 - if peerID == "" { 990 - continue 991 - } 992 - if !shouldAutoReplyMAEPTopic(event.Topic) { 993 - logger.Debug("telegram_maep_ignore_topic", "from_peer_id", peerID, "topic", event.Topic) 994 - continue 995 - } 996 - task, sessionID := extractMAEPTask(event) 997 - if strings.TrimSpace(task) == "" { 998 - logger.Debug("telegram_maep_ignore_empty_task", "from_peer_id", peerID, "topic", event.Topic) 999 - continue 1000 - } 1001 - sessionKey := maepSessionKey(peerID, event.Topic, sessionID) 1002 - 1003 - maepMu.Lock() 1004 - historySnapshot := append([]llm.Message(nil), maepHistory[peerID]...) 1005 - maepMu.Unlock() 1006 - 1007 - feedback := maepFeedbackClassification{ 1008 - NextAction: "continue", 1009 - Confidence: 1, 1010 - } 1011 - feedbackCtx, feedbackCancel := context.WithTimeout(context.Background(), maepFeedbackTimeout(requestTimeout)) 1012 - classified, classifyErr := classifyMAEPFeedback(feedbackCtx, client, model, historySnapshot, task) 1013 - feedbackCancel() 1014 - if classifyErr != nil { 1015 - logger.Warn("telegram_maep_feedback_classify_error", "from_peer_id", peerID, "topic", event.Topic, "error", classifyErr.Error()) 1016 - } else { 1017 - feedback = classified 1018 - } 1019 - 1020 - now := time.Now().UTC() 1021 - if err := applyMAEPInboundFeedback(context.Background(), contactsSvc, maepSvc, peerID, event.Topic, sessionID, feedback, now); err != nil { 1022 - logger.Warn("contacts_feedback_maep_error", "peer_id", peerID, "topic", event.Topic, "error", err.Error()) 1023 - } 1024 - maepMu.Lock() 1025 - sessionState := maepSessions[sessionKey] 1026 - sessionState = applyMAEPFeedback(sessionState, feedback) 1027 - nextSessionState, blockedByFeedback, blockedReason := maybeLimitMAEPSessionByFeedback(now, sessionState, feedback, maepSessionCooldown) 1028 - if !blockedByFeedback { 1029 - var allowedTurn bool 1030 - nextSessionState, allowedTurn = allowMAEPSessionTurn(now, nextSessionState, maepMaxTurnsPerSession, maepSessionCooldown) 1031 - if !allowedTurn { 1032 - blockedByFeedback = true 1033 - blockedReason = "turn_limit_or_cooldown" 1034 - } 1035 - } 1036 - shouldRefreshPreferences := false 1037 - if blockedByFeedback && !nextSessionState.PreferenceSynced { 1038 - nextSessionState.PreferenceSynced = true 1039 - shouldRefreshPreferences = true 1040 - } 1041 - maepSessions[sessionKey] = nextSessionState 1042 - maepSessionDirty = true 1043 - if blockedByFeedback { 1044 - maepMu.Unlock() 1045 - preferenceChanged := false 1046 - if shouldRefreshPreferences { 1047 - prefCtx, prefCancel := context.WithTimeout(context.Background(), maepFeedbackTimeout(requestTimeout)) 1048 - changed, prefErr := refreshMAEPPreferencesOnSessionEnd(prefCtx, contactsSvc, maepSvc, client, model, peerID, event.Topic, sessionID, task, historySnapshot, now, blockedReason) 1049 - prefCancel() 1050 - if prefErr != nil { 1051 - logger.Warn("telegram_maep_preference_refresh_error", "from_peer_id", peerID, "topic", event.Topic, "session_key", sessionKey, "reason", blockedReason, "error", prefErr.Error()) 1052 - } else { 1053 - preferenceChanged = changed 1054 - } 1055 - } 1056 - logger.Info( 1057 - "telegram_maep_session_limited", 1058 - "from_peer_id", peerID, 1059 - "session_key", sessionKey, 1060 - "reason", blockedReason, 1061 - "turn_count", nextSessionState.TurnCount, 1062 - "interest_level", fmt.Sprintf("%.3f", nextSessionState.InterestLevel), 1063 - "low_interest_rounds", nextSessionState.LowInterestRounds, 1064 - "cooldown_until", nextSessionState.CooldownUntil.UTC().Format(time.RFC3339), 1065 - "preference_refresh_attempted", shouldRefreshPreferences, 1066 - "preference_changed", preferenceChanged, 1067 - ) 1068 - continue 1069 - } 1070 - h := append([]llm.Message(nil), maepHistory[peerID]...) 1071 - sticky := append([]string(nil), maepStickySkills[peerID]...) 1072 - maepVersion++ 1073 - currentVersion := maepVersion 1074 - maepMu.Unlock() 1075 - 1076 - logger.Info("telegram_maep_task_enqueued", "from_peer_id", peerID, "topic", event.Topic, "task_len", len(task)) 1077 - runCtx, cancel := context.WithTimeout(context.Background(), taskTimeout) 1078 - final, _, loadedSkills, runErr := runMAEPTask(runCtx, d, logger, logOpts, client, reg, sharedGuard, cfg, model, peerID, h, sticky, taskRuntimeOpts, task) 1079 - cancel() 1080 - if runErr != nil { 1081 - logger.Warn("telegram_maep_task_error", "from_peer_id", peerID, "topic", event.Topic, "error", runErr.Error()) 1082 - continue 1083 - } 1084 - 1085 - output := strings.TrimSpace(formatFinalOutput(final)) 1086 - if output == "" { 1087 - continue 1088 - } 1089 - pushCtx, pushCancel := context.WithTimeout(context.Background(), maepPushTimeout(requestTimeout)) 1090 - replyTopic := resolveMAEPReplyTopic(event.Topic) 1091 - replyMessageID, pushErr := publishMAEPBusOutbound(pushCtx, inprocBus, peerID, replyTopic, output, sessionID, event.ReplyTo, fmt.Sprintf("maep:reply:%s", peerID)) 1092 - pushCancel() 1093 - if pushErr != nil { 1094 - logger.Warn("telegram_maep_reply_queue_error", "to_peer_id", peerID, "topic", replyTopic, "bus_error_code", busErrorCodeString(pushErr), "error", pushErr.Error()) 1095 - continue 1096 - } 1097 - logger.Info("telegram_maep_reply_queued", "to_peer_id", peerID, "topic", replyTopic, "message_id", replyMessageID) 1098 - 1099 - maepMu.Lock() 1100 - if currentVersion == maepVersion { 1101 - sessionState = maepSessions[sessionKey] 1102 - sessionState.TurnCount++ 1103 - sessionState.UpdatedAt = time.Now().UTC() 1104 - maepSessions[sessionKey] = sessionState 1105 - maepSessionDirty = true 1106 - if len(loadedSkills) > 0 { 1107 - maepStickySkills[peerID] = capUniqueStrings(loadedSkills, telegramStickySkillsCap) 1108 - } 1109 - cur := maepHistory[peerID] 1110 - cur = append(cur, 1111 - llm.Message{Role: "user", Content: task}, 1112 - llm.Message{Role: "assistant", Content: output}, 1113 - ) 1114 - if len(cur) > telegramHistoryCap { 1115 - cur = cur[len(cur)-telegramHistoryCap:] 1116 - } 1117 - maepHistory[peerID] = cur 1118 - } 1119 - maepMu.Unlock() 1120 - } 1121 - } 1122 - }() 1123 - } 1124 - 1125 857 for { 1126 858 updates, nextOffset, err := api.getUpdates(pollCtx, offset, pollTimeout) 1127 859 if err != nil { ··· 1137 869 time.Sleep(1 * time.Second) 1138 870 continue 1139 871 } 1140 - offsetChanged := nextOffset != offset 1141 872 offset = nextOffset 1142 873 1143 874 for _, u := range updates { ··· 1601 1332 } 1602 1333 } 1603 1334 1604 - persistNeeded := offsetChanged 1605 - var maepSessionsSnapshot map[string]maepSessionState 1606 - maepMu.Lock() 1607 - if maepSessionDirty { 1608 - persistNeeded = true 1609 - maepSessionDirty = false 1610 - } 1611 - if persistNeeded { 1612 - maepSessionsSnapshot = cloneMAEPSessionStates(maepSessions) 1613 - } 1614 - maepMu.Unlock() 1615 - 1616 - if persistNeeded { 1617 - snapshot := maepruntime.StateSnapshot{ 1618 - ChannelOffsets: map[string]int64{ 1619 - maepruntime.ChannelTelegram: offset, 1620 - }, 1621 - SessionStates: exportMAEPSessionStates(maepSessionsSnapshot), 1622 - } 1623 - if err := runtimeStore.Save(snapshot); err != nil { 1624 - logger.Warn("telegram_runtime_state_persist_error", "error", err.Error()) 1625 - } 1626 - } 1627 1335 } 1628 1336 } 1629 1337
-486
internal/channelruntime/telegram/runtime_helpers.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 - "encoding/base64" 7 6 "encoding/json" 8 7 "fmt" 9 8 "io" ··· 21 20 busruntime "github.com/quailyquaily/mistermorph/internal/bus" 22 21 "github.com/quailyquaily/mistermorph/internal/chathistory" 23 22 "github.com/quailyquaily/mistermorph/internal/idempotency" 24 - "github.com/quailyquaily/mistermorph/internal/jsonutil" 25 - "github.com/quailyquaily/mistermorph/internal/llminspect" 26 - "github.com/quailyquaily/mistermorph/internal/maepruntime" 27 - "github.com/quailyquaily/mistermorph/llm" 28 - "github.com/quailyquaily/mistermorph/maep" 29 23 ) 30 24 31 25 func splitCommand(text string) (cmd string, rest string) { ··· 155 149 return chatType == "group" || chatType == "supergroup" 156 150 } 157 151 158 - func shouldAutoReplyMAEPTopic(topic string) bool { 159 - return strings.TrimSpace(topic) != "" 160 - } 161 - 162 - func resolveMAEPReplyTopic(inboundTopic string) string { 163 - switch strings.ToLower(strings.TrimSpace(inboundTopic)) { 164 - case "dm.checkin.v1": 165 - return "dm.reply.v1" 166 - case "chat.message": 167 - return "chat.message" 168 - default: 169 - return "dm.reply.v1" 170 - } 171 - } 172 - 173 152 func busErrorCodeString(err error) string { 174 153 if err == nil { 175 154 return "" ··· 235 214 return messageID, nil 236 215 } 237 216 238 - func publishMAEPBusOutbound(ctx context.Context, inprocBus *busruntime.Inproc, peerID string, topic string, text string, sessionID string, replyTo string, correlationID string) (string, error) { 239 - if inprocBus == nil { 240 - return "", fmt.Errorf("bus is required") 241 - } 242 - if ctx == nil { 243 - return "", fmt.Errorf("context is required") 244 - } 245 - peerID = strings.TrimSpace(peerID) 246 - if peerID == "" { 247 - return "", fmt.Errorf("peer_id is required") 248 - } 249 - topic = strings.TrimSpace(topic) 250 - if topic == "" { 251 - return "", fmt.Errorf("topic is required") 252 - } 253 - text = strings.TrimSpace(text) 254 - if text == "" { 255 - return "", fmt.Errorf("text is required") 256 - } 257 - sessionID = strings.TrimSpace(sessionID) 258 - replyTo = strings.TrimSpace(replyTo) 259 - now := time.Now().UTC() 260 - messageID := "msg_" + uuid.NewString() 261 - payloadBase64, err := busruntime.EncodeMessageEnvelope(topic, busruntime.MessageEnvelope{ 262 - MessageID: messageID, 263 - Text: text, 264 - SentAt: now.Format(time.RFC3339), 265 - SessionID: sessionID, 266 - ReplyTo: replyTo, 267 - }) 268 - if err != nil { 269 - return "", err 270 - } 271 - conversationKey, err := busruntime.BuildMAEPPeerConversationKey(peerID) 272 - if err != nil { 273 - return "", err 274 - } 275 - correlationID = strings.TrimSpace(correlationID) 276 - if correlationID == "" { 277 - correlationID = "maep:" + messageID 278 - } 279 - outbound := busruntime.BusMessage{ 280 - ID: "bus_" + uuid.NewString(), 281 - Direction: busruntime.DirectionOutbound, 282 - Channel: busruntime.ChannelMAEP, 283 - Topic: topic, 284 - ConversationKey: conversationKey, 285 - ParticipantKey: peerID, 286 - IdempotencyKey: idempotency.MessageEnvelopeKey(messageID), 287 - CorrelationID: correlationID, 288 - PayloadBase64: payloadBase64, 289 - CreatedAt: now, 290 - Extensions: busruntime.MessageExtensions{ 291 - SessionID: sessionID, 292 - ReplyTo: replyTo, 293 - }, 294 - } 295 - if err := inprocBus.PublishValidated(ctx, outbound); err != nil { 296 - return "", err 297 - } 298 - return messageID, nil 299 - } 300 - 301 - func extractMAEPTask(event maep.DataPushEvent) (string, string) { 302 - payload := event.PayloadBytes 303 - if len(payload) == 0 { 304 - decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(event.PayloadBase64)) 305 - if err == nil { 306 - payload = decoded 307 - } 308 - } 309 - if len(payload) == 0 { 310 - return "", "" 311 - } 312 - contentType := strings.ToLower(strings.TrimSpace(event.ContentType)) 313 - if strings.HasPrefix(contentType, "text/") { 314 - return strings.TrimSpace(string(payload)), strings.TrimSpace(event.SessionID) 315 - } 316 - if strings.HasPrefix(contentType, "application/json") { 317 - var obj map[string]any 318 - if err := json.Unmarshal(payload, &obj); err != nil { 319 - return strings.TrimSpace(string(payload)), "" 320 - } 321 - task := "" 322 - for _, key := range []string{"text", "message", "content", "prompt"} { 323 - if v, ok := obj[key].(string); ok && strings.TrimSpace(v) != "" { 324 - task = strings.TrimSpace(v) 325 - break 326 - } 327 - } 328 - sessionID := "" 329 - if v, ok := obj["session_id"].(string); ok { 330 - sessionID = strings.TrimSpace(v) 331 - } 332 - if sessionID == "" { 333 - sessionID = strings.TrimSpace(event.SessionID) 334 - } 335 - return task, sessionID 336 - } 337 - return "", "" 338 - } 339 - 340 - func classifyMAEPFeedback(ctx context.Context, client llm.Client, model string, history []llm.Message, inboundText string) (maepFeedbackClassification, error) { 341 - feedback := maepFeedbackClassification{ 342 - NextAction: "continue", 343 - Confidence: 1, 344 - } 345 - if client == nil { 346 - return feedback, nil 347 - } 348 - model = strings.TrimSpace(model) 349 - if model == "" { 350 - return feedback, nil 351 - } 352 - inboundText = strings.TrimSpace(inboundText) 353 - if inboundText == "" { 354 - return feedback, nil 355 - } 356 - recent := history 357 - if len(recent) > 8 { 358 - recent = recent[len(recent)-8:] 359 - } 360 - systemPrompt, userPrompt, err := renderMAEPFeedbackPrompts(recent, inboundText) 361 - if err != nil { 362 - return feedback, fmt.Errorf("render maep feedback prompts: %w", err) 363 - } 364 - res, err := client.Chat(llminspect.WithModelScene(ctx, "maep.feedback_classify"), llm.Request{ 365 - Model: model, 366 - ForceJSON: true, 367 - Messages: []llm.Message{ 368 - {Role: "system", Content: systemPrompt}, 369 - {Role: "user", Content: userPrompt}, 370 - }, 371 - Parameters: map[string]any{ 372 - "temperature": 0, 373 - "max_tokens": 400, 374 - }, 375 - }) 376 - if err != nil { 377 - return feedback, err 378 - } 379 - raw := strings.TrimSpace(res.Text) 380 - if raw == "" { 381 - return feedback, fmt.Errorf("empty feedback classification") 382 - } 383 - if err := jsonutil.DecodeWithFallback(raw, &feedback); err != nil { 384 - return feedback, err 385 - } 386 - return normalizeMAEPFeedback(feedback), nil 387 - } 388 - 389 - func normalizeMAEPFeedback(v maepFeedbackClassification) maepFeedbackClassification { 390 - v.SignalPositive = clampUnit(v.SignalPositive) 391 - v.SignalNegative = clampUnit(v.SignalNegative) 392 - v.SignalBored = clampUnit(v.SignalBored) 393 - v.Confidence = clampUnit(v.Confidence) 394 - v.NextAction = normalizeMAEPNextAction(v.NextAction) 395 - if v.NextAction == "" { 396 - v.NextAction = "continue" 397 - } 398 - return v 399 - } 400 - 401 - func normalizeMAEPNextAction(v string) string { 402 - switch strings.ToLower(strings.TrimSpace(v)) { 403 - case "continue": 404 - return "continue" 405 - case "wrap_up": 406 - return "wrap_up" 407 - case "switch_topic": 408 - return "switch_topic" 409 - default: 410 - return "" 411 - } 412 - } 413 - 414 - func maepFeedbackTimeout(requestTimeout time.Duration) time.Duration { 415 - if requestTimeout > 0 { 416 - timeout := requestTimeout / 3 417 - if timeout < 3*time.Second { 418 - timeout = 3 * time.Second 419 - } 420 - if timeout > 15*time.Second { 421 - timeout = 15 * time.Second 422 - } 423 - return timeout 424 - } 425 - return 5 * time.Second 426 - } 427 - 428 - func maepPushTimeout(requestTimeout time.Duration) time.Duration { 429 - if requestTimeout > 0 { 430 - return requestTimeout 431 - } 432 - return 15 * time.Second 433 - } 434 - 435 - func maepSessionKey(peerID string, topic string, sessionID string) string { 436 - p := strings.TrimSpace(peerID) 437 - if p == "" { 438 - p = "unknown" 439 - } 440 - s := strings.TrimSpace(sessionID) 441 - if s != "" { 442 - return p + "::session:" + s 443 - } 444 - return p + "::" + maepSessionScopeByTopic(topic) 445 - } 446 - 447 - func maepSessionScopeByTopic(topic string) string { 448 - return maep.SessionScopeByTopic(topic) 449 - } 450 - 451 - func cloneMAEPSessionStates(in map[string]maepSessionState) map[string]maepSessionState { 452 - if len(in) == 0 { 453 - return map[string]maepSessionState{} 454 - } 455 - out := make(map[string]maepSessionState, len(in)) 456 - for key, value := range in { 457 - out[key] = value 458 - } 459 - return out 460 - } 461 - 462 - func exportMAEPSessionStates(in map[string]maepSessionState) map[string]maepruntime.SessionState { 463 - if len(in) == 0 { 464 - return map[string]maepruntime.SessionState{} 465 - } 466 - out := make(map[string]maepruntime.SessionState, len(in)) 467 - for key, value := range in { 468 - out[key] = maepruntime.SessionState{ 469 - TurnCount: value.TurnCount, 470 - CooldownUntil: value.CooldownUntil, 471 - UpdatedAt: value.UpdatedAt, 472 - InterestLevel: value.InterestLevel, 473 - LowInterestRounds: value.LowInterestRounds, 474 - PreferenceSynced: value.PreferenceSynced, 475 - } 476 - } 477 - return out 478 - } 479 - 480 - func restoreMAEPSessionStates(in map[string]maepruntime.SessionState) map[string]maepSessionState { 481 - if len(in) == 0 { 482 - return map[string]maepSessionState{} 483 - } 484 - out := make(map[string]maepSessionState, len(in)) 485 - for key, value := range in { 486 - out[key] = maepSessionState{ 487 - TurnCount: value.TurnCount, 488 - CooldownUntil: value.CooldownUntil, 489 - UpdatedAt: value.UpdatedAt, 490 - InterestLevel: value.InterestLevel, 491 - LowInterestRounds: value.LowInterestRounds, 492 - PreferenceSynced: value.PreferenceSynced, 493 - } 494 - } 495 - return out 496 - } 497 - 498 - func applyMAEPFeedback(state maepSessionState, feedback maepFeedbackClassification) maepSessionState { 499 - state.InterestLevel = clampUnit(state.InterestLevel) 500 - if state.InterestLevel == 0 { 501 - state.InterestLevel = defaultMAEPInterestLevel 502 - } 503 - feedback = normalizeMAEPFeedback(feedback) 504 - next := state.InterestLevel + 0.35*feedback.SignalPositive - 0.30*feedback.SignalNegative - 0.10*feedback.SignalBored 505 - state.InterestLevel = clampUnit(next) 506 - if state.InterestLevel < maepInterestStopThreshold { 507 - state.LowInterestRounds++ 508 - } else { 509 - state.LowInterestRounds = 0 510 - } 511 - return state 512 - } 513 - 514 - func maybeLimitMAEPSessionByFeedback(now time.Time, state maepSessionState, feedback maepFeedbackClassification, cooldown time.Duration) (maepSessionState, bool, string) { 515 - if now.IsZero() { 516 - now = time.Now().UTC() 517 - } 518 - if cooldown <= 0 { 519 - cooldown = defaultMAEPSessionCooldown 520 - } 521 - feedback = normalizeMAEPFeedback(feedback) 522 - if feedback.NextAction == "wrap_up" && feedback.Confidence >= maepWrapUpConfidenceThreshold { 523 - state.CooldownUntil = now.Add(cooldown) 524 - state.UpdatedAt = now 525 - return state, true, "feedback_wrap_up" 526 - } 527 - if state.LowInterestRounds >= maepInterestLowRoundsLimit { 528 - state.CooldownUntil = now.Add(cooldown) 529 - state.UpdatedAt = now 530 - return state, true, "feedback_low_interest" 531 - } 532 - return state, false, "" 533 - } 534 - 535 - func allowMAEPSessionTurn(now time.Time, state maepSessionState, maxTurns int, cooldown time.Duration) (maepSessionState, bool) { 536 - if now.IsZero() { 537 - now = time.Now().UTC() 538 - } 539 - if maxTurns <= 0 { 540 - maxTurns = defaultMAEPMaxTurnsPerSession 541 - } 542 - if cooldown <= 0 { 543 - cooldown = defaultMAEPSessionCooldown 544 - } 545 - 546 - if !state.CooldownUntil.IsZero() { 547 - if now.Before(state.CooldownUntil) { 548 - state.UpdatedAt = now 549 - return state, false 550 - } 551 - state.CooldownUntil = time.Time{} 552 - state.TurnCount = 0 553 - state.LowInterestRounds = 0 554 - state.InterestLevel = defaultMAEPInterestLevel 555 - state.PreferenceSynced = false 556 - } 557 - 558 - if state.TurnCount >= maxTurns { 559 - state.CooldownUntil = now.Add(cooldown) 560 - state.UpdatedAt = now 561 - return state, false 562 - } 563 - 564 - state.InterestLevel = clampUnit(state.InterestLevel) 565 - if state.InterestLevel == 0 { 566 - state.InterestLevel = defaultMAEPInterestLevel 567 - } 568 - if state.LowInterestRounds < 0 { 569 - state.LowInterestRounds = 0 570 - } 571 - state.UpdatedAt = now 572 - return state, true 573 - } 574 - 575 - func applyMAEPInboundFeedback( 576 - ctx context.Context, 577 - contactsSvc *contacts.Service, 578 - maepSvc *maep.Service, 579 - peerID string, 580 - inboundTopic string, 581 - sessionID string, 582 - feedback maepFeedbackClassification, 583 - now time.Time, 584 - ) error { 585 - _ = ctx 586 - _ = contactsSvc 587 - _ = maepSvc 588 - _ = peerID 589 - _ = inboundTopic 590 - _ = sessionID 591 - _ = feedback 592 - _ = now 593 - return nil 594 - } 595 - 596 217 func applyTelegramInboundFeedback( 597 218 ctx context.Context, 598 219 svc *contacts.Service, ··· 633 254 return fmt.Sprintf("tg:%d", userID) 634 255 } 635 256 return "" 636 - } 637 - 638 - func chooseBusinessContactID(nodeID string, peerID string) string { 639 - nodeID = strings.TrimSpace(nodeID) 640 - if nodeID != "" { 641 - return nodeID 642 - } 643 - peerID = strings.TrimSpace(peerID) 644 - if peerID == "" { 645 - return "" 646 - } 647 - return "maep:" + peerID 648 - } 649 - 650 - func refreshMAEPPreferencesOnSessionEnd( 651 - ctx context.Context, 652 - contactsSvc *contacts.Service, 653 - maepSvc *maep.Service, 654 - client llm.Client, 655 - model string, 656 - peerID string, 657 - inboundTopic string, 658 - sessionID string, 659 - latestTask string, 660 - history []llm.Message, 661 - now time.Time, 662 - reason string, 663 - ) (bool, error) { 664 - _ = ctx 665 - _ = contactsSvc 666 - _ = maepSvc 667 - _ = client 668 - _ = model 669 - _ = peerID 670 - _ = inboundTopic 671 - _ = sessionID 672 - _ = latestTask 673 - _ = history 674 - _ = now 675 - _ = reason 676 - return false, nil 677 - } 678 - 679 - func lookupMAEPBusinessContact(ctx context.Context, maepSvc *maep.Service, contactsSvc *contacts.Service, peerID string) (contacts.Contact, bool, error) { 680 - if contactsSvc == nil { 681 - return contacts.Contact{}, false, nil 682 - } 683 - peerID = strings.TrimSpace(peerID) 684 - if peerID == "" { 685 - return contacts.Contact{}, false, nil 686 - } 687 - nodeID := "" 688 - if maepSvc != nil { 689 - item, ok, err := maepSvc.GetContactByPeerID(ctx, peerID) 690 - if err != nil { 691 - return contacts.Contact{}, false, err 692 - } 693 - if ok { 694 - nodeID = strings.TrimSpace(item.NodeID) 695 - } 696 - } 697 - ids := []string{chooseBusinessContactID(nodeID, peerID), "maep:" + peerID} 698 - seen := map[string]bool{} 699 - for _, raw := range ids { 700 - contactID := strings.TrimSpace(raw) 701 - if contactID == "" || seen[contactID] { 702 - continue 703 - } 704 - seen[contactID] = true 705 - contact, ok, err := contactsSvc.GetContact(ctx, contactID) 706 - if err != nil { 707 - return contacts.Contact{}, false, err 708 - } 709 - if ok { 710 - return contact, true, nil 711 - } 712 - } 713 - return contacts.Contact{}, false, nil 714 257 } 715 258 716 259 const ( ··· 989 532 item := newTelegramOutboundAgentHistoryItem(chatID, chatType, text, sentAt, botUser) 990 533 item.Kind = chathistory.KindSystem 991 534 return item 992 - } 993 - 994 - func collectMAEPUserUtterances(history []llm.Message, latestTask string) []string { 995 - values := make([]string, 0, len(history)+1) 996 - for _, msg := range history { 997 - if strings.ToLower(strings.TrimSpace(msg.Role)) != "user" { 998 - continue 999 - } 1000 - text := strings.TrimSpace(msg.Content) 1001 - if text == "" { 1002 - continue 1003 - } 1004 - values = append(values, text) 1005 - } 1006 - if text := strings.TrimSpace(latestTask); text != "" { 1007 - values = append(values, text) 1008 - } 1009 - if len(values) == 0 { 1010 - return nil 1011 - } 1012 - out := make([]string, 0, len(values)) 1013 - for _, raw := range values { 1014 - text := strings.TrimSpace(raw) 1015 - if text == "" { 1016 - continue 1017 - } 1018 - out = append(out, text) 1019 - } 1020 - return out 1021 535 } 1022 536 1023 537 func dedupeNonEmptyStrings(values []string) []string {
-15
internal/channelruntime/telegram/runtime_options.go
··· 11 11 GroupTriggerMode string 12 12 AddressingConfidenceThreshold float64 13 13 AddressingInterjectThreshold float64 14 - WithMAEP bool 15 - MAEPListenAddrs []string 16 14 PollTimeout time.Duration 17 15 TaskTimeout time.Duration 18 16 MaxConcurrency int ··· 36 34 MemoryInjectionEnabled bool 37 35 MemoryInjectionMaxItems int 38 36 SecretsRequireSkillProfiles bool 39 - MAEPMaxTurnsPerSession int 40 - MAEPSessionCooldown time.Duration 41 37 InspectPrompt bool 42 38 InspectRequest bool 43 39 } ··· 49 45 GroupTriggerMode: strings.TrimSpace(opts.GroupTriggerMode), 50 46 AddressingConfidenceThreshold: opts.AddressingConfidenceThreshold, 51 47 AddressingInterjectThreshold: opts.AddressingInterjectThreshold, 52 - WithMAEP: opts.WithMAEP, 53 - MAEPListenAddrs: normalizeRunStringSlice(opts.MAEPListenAddrs), 54 48 PollTimeout: opts.PollTimeout, 55 49 TaskTimeout: opts.TaskTimeout, 56 50 MaxConcurrency: opts.MaxConcurrency, ··· 74 68 MemoryInjectionEnabled: opts.MemoryInjectionEnabled, 75 69 MemoryInjectionMaxItems: opts.MemoryInjectionMaxItems, 76 70 SecretsRequireSkillProfiles: opts.SecretsRequireSkillProfiles, 77 - MAEPMaxTurnsPerSession: opts.MAEPMaxTurnsPerSession, 78 - MAEPSessionCooldown: opts.MAEPSessionCooldown, 79 71 InspectPrompt: opts.InspectPrompt, 80 72 InspectRequest: opts.InspectRequest, 81 73 } ··· 85 77 func normalizeRuntimeLoopOptions(opts runtimeLoopOptions) runtimeLoopOptions { 86 78 opts.BotToken = strings.TrimSpace(opts.BotToken) 87 79 opts.AllowedChatIDs = normalizeAllowedChatIDs(opts.AllowedChatIDs) 88 - opts.MAEPListenAddrs = normalizeRunStringSlice(opts.MAEPListenAddrs) 89 80 opts.GroupTriggerMode = strings.ToLower(strings.TrimSpace(opts.GroupTriggerMode)) 90 81 opts.FileCacheDir = strings.TrimSpace(opts.FileCacheDir) 91 82 opts.ServerListen = strings.TrimSpace(opts.ServerListen) ··· 138 129 } 139 130 if opts.GroupTriggerMode == "" { 140 131 opts.GroupTriggerMode = "smart" 141 - } 142 - if opts.MAEPMaxTurnsPerSession <= 0 { 143 - opts.MAEPMaxTurnsPerSession = defaultMAEPMaxTurnsPerSession 144 - } 145 - if opts.MAEPSessionCooldown <= 0 { 146 - opts.MAEPSessionCooldown = defaultMAEPSessionCooldown 147 132 } 148 133 if opts.ServerListen == "" { 149 134 opts.ServerListen = "127.0.0.1:8787"
-13
internal/channelruntime/telegram/runtime_options_test.go
··· 12 12 GroupTriggerMode: "smart", 13 13 AddressingConfidenceThreshold: 0.7, 14 14 AddressingInterjectThreshold: 0.3, 15 - WithMAEP: true, 16 - MAEPListenAddrs: []string{" /ip4/1 ", "/ip4/1"}, 17 15 PollTimeout: 45 * time.Second, 18 16 TaskTimeout: 2 * time.Minute, 19 17 MaxConcurrency: 5, ··· 34 32 MemoryInjectionEnabled: true, 35 33 MemoryInjectionMaxItems: 10, 36 34 SecretsRequireSkillProfiles: true, 37 - MAEPMaxTurnsPerSession: 9, 38 - MAEPSessionCooldown: 12 * time.Hour, 39 35 InspectPrompt: true, 40 36 InspectRequest: true, 41 37 }) ··· 44 40 } 45 41 if len(got.AllowedChatIDs) != 2 || got.AllowedChatIDs[0] != 1 || got.AllowedChatIDs[1] != 2 { 46 42 t.Fatalf("allowed chat ids = %#v, want [1 2]", got.AllowedChatIDs) 47 - } 48 - if len(got.MAEPListenAddrs) != 1 || got.MAEPListenAddrs[0] != "/ip4/1" { 49 - t.Fatalf("maep listen addrs = %#v, want [/ip4/1]", got.MAEPListenAddrs) 50 43 } 51 44 if got.BusMaxInFlight != 2048 || got.AgentMaxSteps != 20 || got.FileCacheMaxFiles != 200 { 52 45 t.Fatalf("resolved options mismatch: %#v", got) ··· 99 92 } 100 93 if got.MemoryInjectionMaxItems != 50 { 101 94 t.Fatalf("memory injection max items = %d, want 50", got.MemoryInjectionMaxItems) 102 - } 103 - if got.MAEPMaxTurnsPerSession != 6 { 104 - t.Fatalf("maep max turns per session = %d, want 6", got.MAEPMaxTurnsPerSession) 105 - } 106 - if got.MAEPSessionCooldown != 72*time.Hour { 107 - t.Fatalf("maep session cooldown = %v, want 72h", got.MAEPSessionCooldown) 108 95 } 109 96 if got.GroupTriggerMode != "smart" { 110 97 t.Fatalf("group trigger mode = %q, want smart", got.GroupTriggerMode)
-59
internal/channelruntime/telegram/runtime_task.go
··· 236 236 return strings.TrimSpace(longTermSubjectID) != "" 237 237 } 238 238 239 - func runMAEPTask(ctx context.Context, d Dependencies, logger *slog.Logger, logOpts agent.LogOptions, client llm.Client, baseReg *tools.Registry, sharedGuard *guard.Guard, cfg agent.Config, model string, peerID string, history []llm.Message, stickySkills []string, runtimeOpts runtimeTaskOptions, task string) (*agent.Final, *agent.Context, []string, error) { 240 - if strings.TrimSpace(task) == "" { 241 - return nil, nil, nil, fmt.Errorf("empty maep task") 242 - } 243 - if baseReg == nil { 244 - baseReg = registryFromDeps(d) 245 - toolsutil.BindTodoUpdateToolLLM(baseReg, client, model) 246 - } 247 - reg := buildMAEPRegistry(baseReg) 248 - registerPlanTool(d, reg, client, model) 249 - toolsutil.BindTodoUpdateToolLLM(reg, client, model) 250 - 251 - promptSpec, loadedSkills, skillAuthProfiles, err := promptSpecForTelegram(d, ctx, logger, logOpts, task, client, model, stickySkills) 252 - if err != nil { 253 - return nil, nil, nil, err 254 - } 255 - promptprofile.ApplyPersonaIdentity(&promptSpec, logger) 256 - promptprofile.AppendLocalToolNotesBlock(&promptSpec, logger) 257 - promptprofile.AppendPlanCreateGuidanceBlock(&promptSpec, reg) 258 - promptprofile.AppendMAEPReplyPolicyBlock(&promptSpec) 259 - 260 - engine := agent.New( 261 - client, 262 - reg, 263 - cfg, 264 - promptSpec, 265 - agent.WithLogger(logger), 266 - agent.WithLogOptions(logOpts), 267 - agent.WithSkillAuthProfiles(skillAuthProfiles, runtimeOpts.SecretsRequireSkillProfiles), 268 - agent.WithGuard(sharedGuard), 269 - ) 270 - final, runCtx, err := engine.Run(ctx, task, agent.RunOptions{ 271 - Model: model, 272 - History: history, 273 - Meta: map[string]any{ 274 - "trigger": "maep_inbound", 275 - }, 276 - }) 277 - if err != nil { 278 - return final, runCtx, loadedSkills, err 279 - } 280 - return final, runCtx, loadedSkills, nil 281 - } 282 - 283 - func buildMAEPRegistry(baseReg *tools.Registry) *tools.Registry { 284 - reg := tools.NewRegistry() 285 - if baseReg == nil { 286 - return reg 287 - } 288 - for _, t := range baseReg.All() { 289 - name := strings.TrimSpace(t.Name()) 290 - if name == "contacts_send" { 291 - continue 292 - } 293 - reg.Register(t) 294 - } 295 - return reg 296 - } 297 - 298 239 func buildTelegramRegistry(baseReg *tools.Registry, chatType string) *tools.Registry { 299 240 reg := tools.NewRegistry() 300 241 if baseReg == nil {
+1 -144
internal/contactsruntime/sender.go
··· 18 18 "github.com/google/uuid" 19 19 "github.com/quailyquaily/mistermorph/contacts" 20 20 busruntime "github.com/quailyquaily/mistermorph/internal/bus" 21 - maepbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/maep" 22 21 slackbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/slack" 23 22 telegrambus "github.com/quailyquaily/mistermorph/internal/bus/adapters/telegram" 24 - "github.com/quailyquaily/mistermorph/internal/pathutil" 25 23 "github.com/quailyquaily/mistermorph/internal/slackclient" 26 - "github.com/quailyquaily/mistermorph/internal/statepaths" 27 - "github.com/quailyquaily/mistermorph/maep" 28 24 ) 29 25 30 26 const defaultTelegramBaseURL = "https://api.telegram.org" 31 27 const defaultSlackBaseURL = "https://slack.com/api" 32 28 33 29 type SenderOptions struct { 34 - MAEPDir string 35 30 TelegramBotToken string 36 31 TelegramBaseURL string 37 32 SlackBotToken string ··· 41 36 } 42 37 43 38 type RoutingSender struct { 44 - maepNode *maep.Node 45 39 bus *busruntime.Inproc 46 40 telegramDelivery *telegrambus.DeliveryAdapter 47 41 slackDelivery *slackbus.DeliveryAdapter 48 - maepDelivery *maepbus.DeliveryAdapter 49 42 telegramClient *http.Client 50 43 slackPoster *slackclient.Client 51 44 telegramBaseURL string 52 45 telegramBotToken string 53 - maepDir string 54 46 logger *slog.Logger 55 - maepMu sync.Mutex 56 47 pendingMu sync.Mutex 57 48 pending map[string]chan deliveryResult 58 49 closeOnce sync.Once ··· 68 59 if ctx == nil { 69 60 return nil, fmt.Errorf("context is required") 70 61 } 71 - dir := strings.TrimSpace(opts.MAEPDir) 72 - if dir == "" { 73 - dir = statepaths.MAEPDir() 74 - } else { 75 - dir = pathutil.ExpandHomePath(dir) 76 - } 77 62 78 63 logger := opts.Logger 79 64 if logger == nil { ··· 108 93 telegramBaseURL: baseURL, 109 94 telegramBotToken: strings.TrimSpace(opts.TelegramBotToken), 110 95 slackPoster: slackclient.New(&http.Client{Timeout: 30 * time.Second}, slackBaseURL, strings.TrimSpace(opts.SlackBotToken)), 111 - maepDir: dir, 112 96 logger: logger, 113 97 pending: make(map[string]chan deliveryResult), 114 98 } ··· 147 131 accepted, deduped, deliverErr = sender.telegramDelivery.Deliver(deliverCtx, msg) 148 132 case busruntime.ChannelSlack: 149 133 accepted, deduped, deliverErr = sender.slackDelivery.Deliver(deliverCtx, msg) 150 - case busruntime.ChannelMAEP: 151 - if err := sender.ensureMAEPDelivery(deliverCtx); err != nil { 152 - deliverErr = err 153 - break 154 - } 155 - accepted, deduped, deliverErr = sender.maepDelivery.Deliver(deliverCtx, msg) 156 134 default: 157 135 deliverErr = fmt.Errorf("unsupported outbound channel: %s", msg.Channel) 158 136 } ··· 183 161 if s.bus != nil { 184 162 _ = s.bus.Close() 185 163 } 186 - if s.maepNode != nil { 187 - _ = s.maepNode.Close() 188 - } 189 164 s.pendingMu.Lock() 190 165 for id, ch := range s.pending { 191 166 delete(s.pending, id) ··· 199 174 return nil 200 175 } 201 176 202 - func (s *RoutingSender) ensureMAEPDelivery(ctx context.Context) error { 203 - if s == nil { 204 - return fmt.Errorf("sender is required") 205 - } 206 - s.maepMu.Lock() 207 - defer s.maepMu.Unlock() 208 - if s.maepDelivery != nil && s.maepNode != nil { 209 - return nil 210 - } 211 - if ctx == nil { 212 - ctx = context.Background() 213 - } 214 - svc := maep.NewService(maep.NewFileStore(s.maepDir)) 215 - node, err := maep.NewNode(ctx, svc, maep.NodeOptions{DialOnly: true, Logger: s.logger}) 216 - if err != nil { 217 - return err 218 - } 219 - delivery, err := maepbus.NewDeliveryAdapter(maepbus.DeliveryAdapterOptions{ 220 - Node: node, 221 - }) 222 - if err != nil { 223 - _ = node.Close() 224 - return err 225 - } 226 - s.maepNode = node 227 - s.maepDelivery = delivery 228 - return nil 229 - } 230 - 231 177 func (s *RoutingSender) Send(ctx context.Context, contact contacts.Contact, decision contacts.ShareDecision) (bool, bool, error) { 232 178 if s == nil { 233 179 return false, false, fmt.Errorf("nil routing sender") ··· 261 207 return false, false, resolveErr 262 208 } 263 209 return s.publishTelegram(ctx, target, decision) 264 - case contacts.ChannelMAEP: 265 - return s.publishMAEP(ctx, contact, decision) 266 210 default: 267 211 return false, false, fmt.Errorf("unsupported delivery channel: %s", channel) 268 212 } 269 213 } 270 214 271 - func (s *RoutingSender) publishMAEP(ctx context.Context, contact contacts.Contact, decision contacts.ShareDecision) (bool, bool, error) { 272 - if s == nil || s.bus == nil { 273 - return false, false, fmt.Errorf("sender bus is not configured") 274 - } 275 - peerID := strings.TrimSpace(decision.PeerID) 276 - if peerID == "" { 277 - peerID = resolveContactMAEPPeerID(contact) 278 - } 279 - if peerID == "" { 280 - return false, false, fmt.Errorf("maep peer_id is required") 281 - } 282 - idempotencyKey := strings.TrimSpace(decision.IdempotencyKey) 283 - if idempotencyKey == "" { 284 - return false, false, fmt.Errorf("idempotency_key is required") 285 - } 286 - topic := contacts.ShareTopic 287 - now := time.Now().UTC() 288 - payloadRaw, err := buildEnvelopePayload(decision, decision.ContentType, decision.PayloadBase64, now) 289 - if err != nil { 290 - return false, false, err 291 - } 292 - payloadBase64 := base64.RawURLEncoding.EncodeToString(payloadRaw) 293 - conversationKey, err := busruntime.BuildMAEPPeerConversationKey(peerID) 294 - if err != nil { 295 - return false, false, err 296 - } 297 - msg := busruntime.BusMessage{ 298 - ID: "bus_" + uuid.NewString(), 299 - Direction: busruntime.DirectionOutbound, 300 - Channel: busruntime.ChannelMAEP, 301 - Topic: topic, 302 - ConversationKey: conversationKey, 303 - ParticipantKey: peerID, 304 - IdempotencyKey: idempotencyKey, 305 - CorrelationID: "contactsruntime:maep:" + idempotencyKey, 306 - PayloadBase64: payloadBase64, 307 - CreatedAt: now, 308 - } 309 - return s.publishAndAwait(ctx, msg) 310 - } 311 - 312 215 func (s *RoutingSender) publishTelegram(ctx context.Context, target any, decision contacts.ShareDecision) (bool, bool, error) { 313 216 if s == nil || s.bus == nil { 314 217 return false, false, fmt.Errorf("sender bus is not configured") ··· 460 363 s.pendingMu.Unlock() 461 364 } 462 365 463 - func buildMAEPDataPushRequest(decision contacts.ShareDecision, now time.Time) (maep.DataPushRequest, error) { 464 - now = now.UTC() 465 - req := maep.DataPushRequest{ 466 - Topic: contacts.ShareTopic, 467 - ContentType: strings.TrimSpace(decision.ContentType), 468 - PayloadBase64: strings.TrimSpace(decision.PayloadBase64), 469 - IdempotencyKey: strings.TrimSpace(decision.IdempotencyKey), 470 - } 471 - envelopePayload, err := buildEnvelopePayload(decision, req.ContentType, req.PayloadBase64, now) 472 - if err != nil { 473 - return maep.DataPushRequest{}, err 474 - } 475 - req.ContentType = "application/json" 476 - req.PayloadBase64 = base64.RawURLEncoding.EncodeToString(envelopePayload) 477 - return req, nil 478 - } 479 - 480 366 func buildEnvelopePayload(decision contacts.ShareDecision, contentType string, payloadBase64 string, now time.Time) ([]byte, error) { 481 367 text, extras, err := decodeEnvelopeTextAndExtras(contentType, payloadBase64) 482 368 if err != nil { ··· 492 378 "sent_at": now.Format(time.RFC3339), 493 379 } 494 380 sessionID := strings.TrimSpace(extras["session_id"]) 495 - if maep.IsDialogueTopic(contacts.ShareTopic) && sessionID == "" { 381 + if sessionID == "" { 496 382 return nil, fmt.Errorf("session_id is required for dialogue topics") 497 383 } 498 384 if sessionID != "" { ··· 935 821 } 936 822 return "private" 937 823 } 938 - 939 - func resolveContactMAEPPeerID(contact contacts.Contact) string { 940 - if _, peerID := splitMAEPNodeID(contact.MAEPNodeID); peerID != "" { 941 - return peerID 942 - } 943 - if _, peerID := splitMAEPNodeID(contact.ContactID); peerID != "" { 944 - return peerID 945 - } 946 - return "" 947 - } 948 - 949 - func splitMAEPNodeID(raw string) (string, string) { 950 - value := strings.TrimSpace(raw) 951 - if value == "" { 952 - return "", "" 953 - } 954 - lower := strings.ToLower(value) 955 - if strings.Contains(value, ":") && !strings.HasPrefix(lower, "maep:") { 956 - return "", "" 957 - } 958 - if strings.HasPrefix(lower, "maep:") { 959 - peerID := strings.TrimSpace(value[len("maep:"):]) 960 - if peerID == "" { 961 - return "", "" 962 - } 963 - return "maep:" + peerID, peerID 964 - } 965 - return "maep:" + value, value 966 - }
+8 -99
internal/contactsruntime/sender_bus_test.go
··· 14 14 "github.com/google/uuid" 15 15 "github.com/quailyquaily/mistermorph/contacts" 16 16 busruntime "github.com/quailyquaily/mistermorph/internal/bus" 17 - maepbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/maep" 18 17 slackbus "github.com/quailyquaily/mistermorph/internal/bus/adapters/slack" 19 18 telegrambus "github.com/quailyquaily/mistermorph/internal/bus/adapters/telegram" 20 - "github.com/quailyquaily/mistermorph/maep" 21 19 ) 22 20 23 21 func TestRoutingSenderSendTelegramViaBus(t *testing.T) { ··· 36 34 return nil 37 35 } 38 36 39 - sender := newRoutingSenderForBusTest(t, sendText, &mockDataPusher{}) 37 + sender := newRoutingSenderForBusTest(t, sendText) 40 38 contentType, payloadBase64 := testEnvelopePayload(t, "hello telegram") 41 39 accepted, deduped, err := sender.Send(ctx, contacts.Contact{ 42 40 ContactID: "tg:12345", ··· 83 81 return nil 84 82 } 85 83 86 - sender := newRoutingSenderForBusTest(t, sendText, &mockDataPusher{}) 84 + sender := newRoutingSenderForBusTest(t, sendText) 87 85 contentType, payloadBase64 := testEnvelopePayload(t, "hello telegram") 88 86 _, _, err := sender.Send(ctx, contacts.Contact{ 89 87 ContactID: "tg:@alice", ··· 123 121 return nil 124 122 } 125 123 126 - sender := newRoutingSenderForBusTest(t, sendText, &mockDataPusher{}) 124 + sender := newRoutingSenderForBusTest(t, sendText) 127 125 contentType, payloadBase64 := testEnvelopePayload(t, "hello telegram") 128 126 _, _, err := sender.Send(ctx, contacts.Contact{ 129 127 ContactID: "tg:@alice", ··· 158 156 return nil 159 157 } 160 158 161 - sender := newRoutingSenderForBusTest(t, sendText, &mockDataPusher{}) 159 + sender := newRoutingSenderForBusTest(t, sendText) 162 160 contentType, payloadBase64 := testEnvelopePayload(t, "hello telegram") 163 161 _, _, err := sender.Send(ctx, contacts.Contact{ 164 162 ContactID: "tg:@alice", ··· 209 207 return fmt.Errorf("unexpected telegram send: target=%v text=%q", target, text) 210 208 } 211 209 212 - sender := newRoutingSenderForBusTest(t, sendTelegram, &mockDataPusher{}, sendSlack) 210 + sender := newRoutingSenderForBusTest(t, sendTelegram, sendSlack) 213 211 contentType, payloadBase64 := testEnvelopePayload(t, "hello slack") 214 212 accepted, deduped, err := sender.Send(ctx, contacts.Contact{ 215 213 ContactID: "slack:T111:U222", ··· 270 268 func(ctx context.Context, target any, text string, opts telegrambus.SendTextOptions) error { 271 269 return fmt.Errorf("unexpected telegram send: target=%v text=%q", target, text) 272 270 }, 273 - &mockDataPusher{}, 274 271 sendSlack, 275 272 ) 276 273 contentType, payloadBase64 := testEnvelopePayload(t, "hello slack by hint") ··· 296 293 } 297 294 } 298 295 299 - func TestRoutingSenderSendMAEPViaBus(t *testing.T) { 300 - ctx := context.Background() 301 - 302 - pusher := &mockDataPusher{ 303 - result: maep.DataPushResult{ 304 - Accepted: true, 305 - Deduped: true, 306 - }, 307 - } 308 - sendText := func(ctx context.Context, target any, text string, opts telegrambus.SendTextOptions) error { 309 - return fmt.Errorf("unexpected telegram send: target=%v text=%q", target, text) 310 - } 311 - sender := newRoutingSenderForBusTest(t, sendText, pusher) 312 - 313 - contentType, payloadBase64 := testEnvelopePayload(t, "hello maep") 314 - accepted, deduped, err := sender.Send(ctx, contacts.Contact{ 315 - ContactID: "maep:12D3KooWTestPeer", 316 - Kind: contacts.KindAgent, 317 - Channel: contacts.ChannelMAEP, 318 - MAEPNodeID: "maep:12D3KooWTestPeer", 319 - }, contacts.ShareDecision{ 320 - ContactID: "maep:12D3KooWTestPeer", 321 - ItemID: "cand_2", 322 - ContentType: contentType, 323 - PayloadBase64: payloadBase64, 324 - IdempotencyKey: "manual:maep:1", 325 - }) 326 - if err != nil { 327 - t.Fatalf("Send() error = %v", err) 328 - } 329 - if !accepted { 330 - t.Fatalf("accepted mismatch: got %v want true", accepted) 331 - } 332 - if !deduped { 333 - t.Fatalf("deduped mismatch: got %v want true", deduped) 334 - } 335 - pusher.mu.Lock() 336 - defer pusher.mu.Unlock() 337 - if pusher.calls != 1 { 338 - t.Fatalf("PushData calls mismatch: got %d want 1", pusher.calls) 339 - } 340 - if pusher.peerID != "12D3KooWTestPeer" { 341 - t.Fatalf("peer_id mismatch: got %q want %q", pusher.peerID, "12D3KooWTestPeer") 342 - } 343 - if pusher.req.Topic != contacts.ShareTopic { 344 - t.Fatalf("topic mismatch: got %q want %q", pusher.req.Topic, contacts.ShareTopic) 345 - } 346 - if pusher.req.IdempotencyKey != "manual:maep:1" { 347 - t.Fatalf("idempotency_key mismatch: got %q want %q", pusher.req.IdempotencyKey, "manual:maep:1") 348 - } 349 - } 350 - 351 296 func TestRoutingSenderSendFailsWithoutIdempotencyKey(t *testing.T) { 352 297 ctx := context.Background() 353 298 354 299 sender := newRoutingSenderForBusTest(t, func(ctx context.Context, target any, text string, opts telegrambus.SendTextOptions) error { 355 300 return nil 356 - }, &mockDataPusher{}) 301 + }) 357 302 contentType, payloadBase64 := testEnvelopePayload(t, "hello") 358 303 _, _, err := sender.Send(ctx, contacts.Contact{ 359 304 ContactID: "tg:12345", ··· 381 326 sender := newRoutingSenderForBusTest(t, func(ctx context.Context, target any, text string, opts telegrambus.SendTextOptions) error { 382 327 calls++ 383 328 return nil 384 - }, &mockDataPusher{}) 329 + }) 385 330 contentType, payloadBase64 := testEnvelopePayload(t, "hello") 386 331 _, _, err := sender.Send(ctx, contacts.Contact{ 387 332 ContactID: "tg:@alice", ··· 406 351 } 407 352 } 408 353 409 - type mockDataPusher struct { 410 - mu sync.Mutex 411 - result maep.DataPushResult 412 - err error 413 - calls int 414 - peerID string 415 - req maep.DataPushRequest 416 - addrs []string 417 - notify bool 418 - context context.Context 419 - } 420 - 421 - func (m *mockDataPusher) PushData(ctx context.Context, peerID string, addresses []string, req maep.DataPushRequest, notification bool) (maep.DataPushResult, error) { 422 - m.mu.Lock() 423 - defer m.mu.Unlock() 424 - m.calls++ 425 - m.peerID = peerID 426 - m.req = req 427 - m.addrs = append([]string(nil), addresses...) 428 - m.notify = notification 429 - m.context = ctx 430 - return m.result, m.err 431 - } 432 - 433 - func newRoutingSenderForBusTest(t *testing.T, sendText telegrambus.SendTextFunc, pusher maepbus.DataPusher, slackSendText ...slackbus.SendTextFunc) *RoutingSender { 354 + func newRoutingSenderForBusTest(t *testing.T, sendText telegrambus.SendTextFunc, slackSendText ...slackbus.SendTextFunc) *RoutingSender { 434 355 t.Helper() 435 356 436 357 if sendText == nil { 437 358 t.Fatalf("sendText is required") 438 - } 439 - if pusher == nil { 440 - t.Fatalf("pusher is required") 441 359 } 442 360 sendSlack := func(ctx context.Context, target any, text string, opts slackbus.SendTextOptions) error { 443 361 return fmt.Errorf("unexpected slack send: target=%v text=%q", target, text) ··· 461 379 if err != nil { 462 380 t.Fatalf("NewDeliveryAdapter(telegram) error = %v", err) 463 381 } 464 - maepDelivery, err := maepbus.NewDeliveryAdapter(maepbus.DeliveryAdapterOptions{ 465 - Node: pusher, 466 - }) 467 - if err != nil { 468 - t.Fatalf("NewDeliveryAdapter(maep) error = %v", err) 469 - } 470 382 slackDelivery, err := slackbus.NewDeliveryAdapter(slackbus.DeliveryAdapterOptions{ 471 383 SendText: sendSlack, 472 384 }) ··· 478 390 bus: bus, 479 391 telegramDelivery: telegramDelivery, 480 392 slackDelivery: slackDelivery, 481 - maepDelivery: maepDelivery, 482 393 pending: make(map[string]chan deliveryResult), 483 394 } 484 395 ··· 500 411 accepted, deduped, deliverErr = sender.telegramDelivery.Deliver(deliverCtx, msg) 501 412 case busruntime.ChannelSlack: 502 413 accepted, deduped, deliverErr = sender.slackDelivery.Deliver(deliverCtx, msg) 503 - case busruntime.ChannelMAEP: 504 - accepted, deduped, deliverErr = sender.maepDelivery.Deliver(deliverCtx, msg) 505 414 default: 506 415 deliverErr = fmt.Errorf("unsupported outbound channel: %s", msg.Channel) 507 416 }
+9 -41
internal/contactsruntime/sender_test.go
··· 9 9 "github.com/quailyquaily/mistermorph/contacts" 10 10 ) 11 11 12 - func TestBuildMAEPDataPushRequest_EnvelopeTopicFromPlainText(t *testing.T) { 12 + func TestBuildEnvelopePayload_RequiresSessionID(t *testing.T) { 13 13 now := time.Date(2026, 2, 7, 4, 31, 30, 0, time.UTC) 14 14 decision := contacts.ShareDecision{ 15 15 ContentType: "text/plain", ··· 18 18 ItemID: "cand_1", 19 19 } 20 20 21 - _, err := buildMAEPDataPushRequest(decision, now) 21 + _, err := buildEnvelopePayload(decision, decision.ContentType, decision.PayloadBase64, now) 22 22 if err == nil { 23 - t.Fatalf("expected error when session_id is missing for dialogue topic") 23 + t.Fatalf("expected error when session_id is missing") 24 24 } 25 25 } 26 26 27 - func TestBuildMAEPDataPushRequest_EnvelopeTopicFromJSON(t *testing.T) { 27 + func TestBuildEnvelopePayload_FromJSON(t *testing.T) { 28 28 now := time.Date(2026, 2, 7, 4, 32, 0, 0, time.UTC) 29 29 payload := map[string]any{ 30 30 "text": "pong", ··· 39 39 ItemID: "", 40 40 } 41 41 42 - req, err := buildMAEPDataPushRequest(decision, now) 42 + raw, err := buildEnvelopePayload(decision, decision.ContentType, decision.PayloadBase64, now) 43 43 if err != nil { 44 - t.Fatalf("buildMAEPDataPushRequest() error = %v", err) 45 - } 46 - if req.Topic != contacts.ShareTopic { 47 - t.Fatalf("Topic mismatch: got %q want %q", req.Topic, contacts.ShareTopic) 48 - } 49 - if req.ContentType != "application/json" { 50 - t.Fatalf("ContentType mismatch: got %q want %q", req.ContentType, "application/json") 51 - } 52 - 53 - raw, err := base64.RawURLEncoding.DecodeString(req.PayloadBase64) 54 - if err != nil { 55 - t.Fatalf("decode payload: %v", err) 44 + t.Fatalf("buildEnvelopePayload() error = %v", err) 56 45 } 57 46 var envelope map[string]any 58 47 if err := json.Unmarshal(raw, &envelope); err != nil { ··· 62 51 t.Fatalf("text mismatch: got %v want pong", got) 63 52 } 64 53 if got := envelope["session_id"]; got != "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456" { 65 - t.Fatalf("session_id mismatch: got %v want 0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", got) 54 + t.Fatalf("session_id mismatch: got %v", got) 66 55 } 67 56 if got := envelope["reply_to"]; got != "msg_prev" { 68 57 t.Fatalf("reply_to mismatch: got %v want msg_prev", got) 69 58 } 70 59 } 71 60 72 - func TestBuildMAEPDataPushRequest_EnvelopeTopicFromPlainTextWithSession(t *testing.T) { 73 - now := time.Date(2026, 2, 7, 4, 33, 0, 0, time.UTC) 74 - payload := map[string]any{ 75 - "text": "x", 76 - "session_id": "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", 77 - } 78 - payloadRaw, _ := json.Marshal(payload) 79 - decision := contacts.ShareDecision{ 80 - ContentType: "application/json", 81 - PayloadBase64: base64.RawURLEncoding.EncodeToString(payloadRaw), 82 - } 83 - 84 - req, err := buildMAEPDataPushRequest(decision, now) 85 - if err != nil { 86 - t.Fatalf("buildMAEPDataPushRequest() error = %v", err) 87 - } 88 - if req.Topic != contacts.ShareTopic { 89 - t.Fatalf("Topic mismatch: got %q want %q", req.Topic, contacts.ShareTopic) 90 - } 91 - } 92 - 93 - func TestBuildMAEPDataPushRequest_InvalidPayload(t *testing.T) { 61 + func TestBuildEnvelopePayload_InvalidPayload(t *testing.T) { 94 62 now := time.Date(2026, 2, 7, 4, 34, 0, 0, time.UTC) 95 63 decision := contacts.ShareDecision{ 96 64 PayloadBase64: "***invalid***", 97 65 } 98 66 99 - if _, err := buildMAEPDataPushRequest(decision, now); err == nil { 67 + if _, err := buildEnvelopePayload(decision, "application/json", decision.PayloadBase64, now); err == nil { 100 68 t.Fatalf("expected error for invalid payload_base64") 101 69 } 102 70 }
-59
internal/maepruntime/runtime.go
··· 1 - package maepruntime 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "log/slog" 7 - "strings" 8 - "time" 9 - 10 - "github.com/quailyquaily/mistermorph/internal/pathutil" 11 - "github.com/quailyquaily/mistermorph/internal/statepaths" 12 - "github.com/quailyquaily/mistermorph/maep" 13 - ) 14 - 15 - type StartOptions struct { 16 - Dir string 17 - ListenAddrs []string 18 - Logger *slog.Logger 19 - OnDataPush func(event maep.DataPushEvent) 20 - } 21 - 22 - func Start(ctx context.Context, opts StartOptions) (*maep.Node, error) { 23 - if ctx == nil { 24 - ctx = context.Background() 25 - } 26 - dir := strings.TrimSpace(opts.Dir) 27 - if dir == "" { 28 - dir = statepaths.MAEPDir() 29 - } else { 30 - dir = pathutil.ExpandHomePath(dir) 31 - } 32 - svc := maep.NewService(maep.NewFileStore(dir)) 33 - if _, _, err := svc.EnsureIdentity(ctx, time.Now().UTC()); err != nil { 34 - return nil, fmt.Errorf("ensure maep identity: %w", err) 35 - } 36 - node, err := maep.NewNode(ctx, svc, maep.NodeOptions{ 37 - ListenAddrs: normalizeListenAddrs(opts.ListenAddrs), 38 - Logger: opts.Logger, 39 - OnDataPush: opts.OnDataPush, 40 - }) 41 - if err != nil { 42 - return nil, err 43 - } 44 - return node, nil 45 - } 46 - 47 - func normalizeListenAddrs(input []string) []string { 48 - out := make([]string, 0, len(input)) 49 - seen := map[string]bool{} 50 - for _, raw := range input { 51 - v := strings.TrimSpace(raw) 52 - if v == "" || seen[v] { 53 - continue 54 - } 55 - seen[v] = true 56 - out = append(out, v) 57 - } 58 - return out 59 - }
-132
internal/maepruntime/state_store.go
··· 1 - package maepruntime 2 - 3 - import ( 4 - "bytes" 5 - "encoding/json" 6 - "fmt" 7 - "io" 8 - "math" 9 - "os" 10 - "path/filepath" 11 - "strings" 12 - "sync" 13 - "time" 14 - 15 - "github.com/quailyquaily/mistermorph/internal/channels" 16 - "github.com/quailyquaily/mistermorph/internal/fsstore" 17 - "github.com/quailyquaily/mistermorph/internal/pathutil" 18 - ) 19 - 20 - const ( 21 - ChannelTelegram = channels.Telegram 22 - ) 23 - 24 - type SessionState struct { 25 - TurnCount int `json:"turn_count"` 26 - CooldownUntil time.Time `json:"cooldown_until,omitempty"` 27 - UpdatedAt time.Time `json:"updated_at,omitempty"` 28 - InterestLevel float64 `json:"interest_level"` 29 - LowInterestRounds int `json:"low_interest_rounds"` 30 - PreferenceSynced bool `json:"preference_synced"` 31 - } 32 - 33 - type StateSnapshot struct { 34 - ChannelOffsets map[string]int64 `json:"channel_offsets"` 35 - SessionStates map[string]SessionState `json:"session_states"` 36 - } 37 - 38 - type StateStore struct { 39 - path string 40 - mu sync.Mutex 41 - } 42 - 43 - func NewStateStore(maepDir string) (*StateStore, error) { 44 - maepDir = strings.TrimSpace(maepDir) 45 - if maepDir == "" { 46 - return nil, fmt.Errorf("maep dir is required") 47 - } 48 - expanded := pathutil.ExpandHomePath(maepDir) 49 - path := filepath.Join(expanded, "runtime", "state.json") 50 - return &StateStore{path: path}, nil 51 - } 52 - 53 - func (s *StateStore) Load() (StateSnapshot, bool, error) { 54 - if s == nil { 55 - return StateSnapshot{}, false, fmt.Errorf("nil state store") 56 - } 57 - s.mu.Lock() 58 - defer s.mu.Unlock() 59 - 60 - raw, err := os.ReadFile(s.path) 61 - if err != nil { 62 - if os.IsNotExist(err) { 63 - return StateSnapshot{}, false, nil 64 - } 65 - return StateSnapshot{}, false, fmt.Errorf("read runtime state %s: %w", s.path, err) 66 - } 67 - if len(bytes.TrimSpace(raw)) == 0 { 68 - return StateSnapshot{}, false, fmt.Errorf("runtime state file is empty: %s", s.path) 69 - } 70 - 71 - dec := json.NewDecoder(bytes.NewReader(raw)) 72 - dec.DisallowUnknownFields() 73 - var snapshot StateSnapshot 74 - if err := dec.Decode(&snapshot); err != nil { 75 - return StateSnapshot{}, false, fmt.Errorf("decode runtime state %s: %w", s.path, err) 76 - } 77 - var trailing any 78 - if err := dec.Decode(&trailing); err != io.EOF { 79 - return StateSnapshot{}, false, fmt.Errorf("decode runtime state %s: trailing data", s.path) 80 - } 81 - if err := validateSnapshot(snapshot); err != nil { 82 - return StateSnapshot{}, false, err 83 - } 84 - return snapshot, true, nil 85 - } 86 - 87 - func (s *StateStore) Save(snapshot StateSnapshot) error { 88 - if s == nil { 89 - return fmt.Errorf("nil state store") 90 - } 91 - if err := validateSnapshot(snapshot); err != nil { 92 - return err 93 - } 94 - s.mu.Lock() 95 - defer s.mu.Unlock() 96 - return fsstore.WriteJSONAtomic(s.path, snapshot, fsstore.FileOptions{}) 97 - } 98 - 99 - func validateSnapshot(snapshot StateSnapshot) error { 100 - if snapshot.ChannelOffsets == nil { 101 - return fmt.Errorf("channel_offsets is required") 102 - } 103 - if snapshot.SessionStates == nil { 104 - return fmt.Errorf("session_states is required") 105 - } 106 - for key, offset := range snapshot.ChannelOffsets { 107 - if key == "" || strings.TrimSpace(key) != key { 108 - return fmt.Errorf("channel offset key is invalid") 109 - } 110 - if offset < 0 { 111 - return fmt.Errorf("channel offset %q must be >= 0", key) 112 - } 113 - } 114 - for key, session := range snapshot.SessionStates { 115 - if key == "" || strings.TrimSpace(key) != key { 116 - return fmt.Errorf("session state key is invalid") 117 - } 118 - if session.TurnCount < 0 { 119 - return fmt.Errorf("session_state[%q].turn_count must be >= 0", key) 120 - } 121 - if session.LowInterestRounds < 0 { 122 - return fmt.Errorf("session_state[%q].low_interest_rounds must be >= 0", key) 123 - } 124 - if math.IsNaN(session.InterestLevel) || math.IsInf(session.InterestLevel, 0) { 125 - return fmt.Errorf("session_state[%q].interest_level must be finite", key) 126 - } 127 - if session.InterestLevel < 0 || session.InterestLevel > 1 { 128 - return fmt.Errorf("session_state[%q].interest_level must be in [0,1]", key) 129 - } 130 - } 131 - return nil 132 - }
-120
internal/maepruntime/state_store_test.go
··· 1 - package maepruntime 2 - 3 - import ( 4 - "os" 5 - "path/filepath" 6 - "strings" 7 - "testing" 8 - "time" 9 - ) 10 - 11 - func TestStateStoreLoadMissing(t *testing.T) { 12 - store, err := NewStateStore(t.TempDir()) 13 - if err != nil { 14 - t.Fatalf("NewStateStore() error = %v", err) 15 - } 16 - snapshot, found, err := store.Load() 17 - if err != nil { 18 - t.Fatalf("Load() error = %v", err) 19 - } 20 - if found { 21 - t.Fatalf("Load() found = true, want false") 22 - } 23 - if snapshot.ChannelOffsets != nil || snapshot.SessionStates != nil { 24 - t.Fatalf("Load() snapshot should be zero value when missing") 25 - } 26 - } 27 - 28 - func TestStateStoreSaveLoadRoundTrip(t *testing.T) { 29 - root := t.TempDir() 30 - store, err := NewStateStore(root) 31 - if err != nil { 32 - t.Fatalf("NewStateStore() error = %v", err) 33 - } 34 - now := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC) 35 - want := StateSnapshot{ 36 - ChannelOffsets: map[string]int64{ 37 - ChannelTelegram: 123, 38 - }, 39 - SessionStates: map[string]SessionState{ 40 - "peerA::session:0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456": { 41 - TurnCount: 2, 42 - CooldownUntil: now.Add(10 * time.Minute), 43 - UpdatedAt: now, 44 - InterestLevel: 0.6, 45 - LowInterestRounds: 1, 46 - PreferenceSynced: true, 47 - }, 48 - }, 49 - } 50 - if err := store.Save(want); err != nil { 51 - t.Fatalf("Save() error = %v", err) 52 - } 53 - 54 - got, found, err := store.Load() 55 - if err != nil { 56 - t.Fatalf("Load() error = %v", err) 57 - } 58 - if !found { 59 - t.Fatalf("Load() found = false, want true") 60 - } 61 - if got.ChannelOffsets[ChannelTelegram] != 123 { 62 - t.Fatalf("channel offset mismatch: got %d want 123", got.ChannelOffsets[ChannelTelegram]) 63 - } 64 - session := got.SessionStates["peerA::session:0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456"] 65 - if session.TurnCount != 2 || session.LowInterestRounds != 1 || session.InterestLevel != 0.6 || !session.PreferenceSynced { 66 - t.Fatalf("session state mismatch: got %+v", session) 67 - } 68 - } 69 - 70 - func TestStateStoreLoadRejectsUnknownField(t *testing.T) { 71 - root := t.TempDir() 72 - path := filepath.Join(root, "runtime", "state.json") 73 - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 74 - t.Fatalf("MkdirAll() error = %v", err) 75 - } 76 - raw := []byte(`{"channel_offsets":{"telegram":1},"session_states":{},"unknown":1}`) 77 - if err := os.WriteFile(path, raw, 0o600); err != nil { 78 - t.Fatalf("WriteFile() error = %v", err) 79 - } 80 - store, err := NewStateStore(root) 81 - if err != nil { 82 - t.Fatalf("NewStateStore() error = %v", err) 83 - } 84 - _, _, err = store.Load() 85 - if err == nil || !strings.Contains(err.Error(), "unknown field") { 86 - t.Fatalf("Load() error = %v, want unknown field", err) 87 - } 88 - } 89 - 90 - func TestStateStoreSaveRejectsInvalidOffset(t *testing.T) { 91 - store, err := NewStateStore(t.TempDir()) 92 - if err != nil { 93 - t.Fatalf("NewStateStore() error = %v", err) 94 - } 95 - err = store.Save(StateSnapshot{ 96 - ChannelOffsets: map[string]int64{ChannelTelegram: -1}, 97 - SessionStates: map[string]SessionState{}, 98 - }) 99 - if err == nil || !strings.Contains(err.Error(), "must be >= 0") { 100 - t.Fatalf("Save() error = %v, want invalid offset", err) 101 - } 102 - } 103 - 104 - func TestStateStoreSaveRejectsInvalidSessionState(t *testing.T) { 105 - store, err := NewStateStore(t.TempDir()) 106 - if err != nil { 107 - t.Fatalf("NewStateStore() error = %v", err) 108 - } 109 - err = store.Save(StateSnapshot{ 110 - ChannelOffsets: map[string]int64{ChannelTelegram: 1}, 111 - SessionStates: map[string]SessionState{ 112 - "peerA::topic:chat.message": { 113 - InterestLevel: 2, 114 - }, 115 - }, 116 - }) 117 - if err == nil || !strings.Contains(err.Error(), "interest_level must be in [0,1]") { 118 - t.Fatalf("Save() error = %v, want interest_level range error", err) 119 - } 120 - }
-18
internal/promptprofile/prompt_blocks.go
··· 20 20 memorySummariesPromptBlockTitle = "Memory Summaries" 21 21 groupUsernamesPromptBlockTitle = "Group Usernames" 22 22 TelegramRuntimePromptBlockTitle = "Telegram Policies" 23 - MAEPReplyPromptBlockTitle = "MAEP Policies" 24 23 ) 25 24 26 25 //go:embed prompts/block_plan_create.tmpl ··· 28 27 29 28 //go:embed prompts/telegram_block.tmpl 30 29 var telegramRuntimePromptBlockTemplateSource string 31 - 32 - //go:embed prompts/maep_block.tmpl 33 - var maepReplyPromptBlockSource string 34 30 35 31 var telegramRuntimePromptBlockTemplate = prompttmpl.MustParse( 36 32 "telegram_runtime_block", ··· 143 139 }) 144 140 } 145 141 } 146 - 147 - func AppendMAEPReplyPolicyBlock(spec *agent.PromptSpec) { 148 - if spec == nil { 149 - return 150 - } 151 - content := strings.TrimSpace(maepReplyPromptBlockSource) 152 - if content == "" { 153 - return 154 - } 155 - spec.Blocks = append(spec.Blocks, agent.PromptBlock{ 156 - Title: MAEPReplyPromptBlockTitle, 157 - Content: content, 158 - }) 159 - }
-4
internal/promptprofile/prompts/maep_block.tmpl
··· 1 - - Your final.output will be sent verbatim to a remote peer as a chat message. 2 - - Reply conversationally and naturally. Do NOT include protocol metadata or operational logs. 3 - - Never mention topics/protocol labels (for example dm.reply.v1, dm.checkin.v1, share.proactive.v1, chat.message), session_id, message_id, peer_id, contact_id, idempotency_key, or tool invocation details. 4 - - Do not report send/retry status, failure causes, or remediation steps unless the peer explicitly asks for diagnostic details.
-8
internal/statepaths/statepaths.go
··· 33 33 ) 34 34 } 35 35 36 - func MAEPDir() string { 37 - return pathutil.ResolveStateChildDir( 38 - viper.GetString("file_state_dir"), 39 - viper.GetString("maep.dir_name"), 40 - "maep", 41 - ) 42 - } 43 - 44 36 func ContactsDir() string { 45 37 return pathutil.ResolveStateChildDir( 46 38 viper.GetString("file_state_dir"),
-323
maep/contact_card.go
··· 1 - package maep 2 - 3 - import ( 4 - "bytes" 5 - "encoding/json" 6 - "fmt" 7 - "net" 8 - "strings" 9 - "time" 10 - 11 - jsoncanonicalizer "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" 12 - "github.com/google/uuid" 13 - ic "github.com/libp2p/go-libp2p/core/crypto" 14 - "github.com/libp2p/go-libp2p/core/peer" 15 - ma "github.com/multiformats/go-multiaddr" 16 - ) 17 - 18 - const ed25519PublicKeyBytes = 32 19 - 20 - func BuildSignedContactCard(identity Identity, addresses []string, minProtocol int, maxProtocol int, issuedAt time.Time, expiresAt *time.Time) (ContactCard, error) { 21 - if minProtocol <= 0 || maxProtocol <= 0 { 22 - return ContactCard{}, WrapProtocolError(ErrInvalidParams, "protocol versions must be positive") 23 - } 24 - if minProtocol > maxProtocol { 25 - return ContactCard{}, WrapProtocolError(ErrInvalidParams, "min_supported_protocol cannot be greater than max_supported_protocol") 26 - } 27 - if issuedAt.IsZero() { 28 - issuedAt = time.Now().UTC() 29 - } 30 - issuedAt = issuedAt.UTC() 31 - 32 - if strings.TrimSpace(identity.NodeUUID) == "" || strings.TrimSpace(identity.PeerID) == "" || strings.TrimSpace(identity.IdentityPubEd25519) == "" { 33 - return ContactCard{}, WrapProtocolError(ErrInvalidParams, "identity is incomplete") 34 - } 35 - 36 - peerID, err := peer.Decode(identity.PeerID) 37 - if err != nil { 38 - return ContactCard{}, WrapProtocolError(ErrInvalidParams, "identity.peer_id is invalid: %v", err) 39 - } 40 - 41 - pubPeerID, err := DerivePeerIDFromIdentityPub(identity.IdentityPubEd25519) 42 - if err != nil { 43 - return ContactCard{}, err 44 - } 45 - if pubPeerID != peerID { 46 - return ContactCard{}, WrapProtocolError(ErrInvalidParams, "identity.peer_id does not match identity_pub_ed25519") 47 - } 48 - 49 - normalizedAddresses := normalizeAddresses(addresses) 50 - if len(normalizedAddresses) == 0 { 51 - return ContactCard{}, WrapProtocolError(ErrInvalidParams, "at least one address is required") 52 - } 53 - for _, addr := range normalizedAddresses { 54 - if err := validateAddressMatchesPeerID(addr, peerID); err != nil { 55 - return ContactCard{}, err 56 - } 57 - } 58 - 59 - payload := ContactCardPayload{ 60 - Version: ContactCardVersionV1, 61 - NodeUUID: strings.TrimSpace(identity.NodeUUID), 62 - PeerID: peerID.String(), 63 - NodeID: NodeIDFromPeerID(peerID.String()), 64 - IdentityPubEd25519: strings.TrimSpace(identity.IdentityPubEd25519), 65 - Addresses: normalizedAddresses, 66 - MinSupportedProtocol: minProtocol, 67 - MaxSupportedProtocol: maxProtocol, 68 - IssuedAt: issuedAt, 69 - ExpiresAt: expiresAt, 70 - } 71 - 72 - payloadRaw, err := json.Marshal(payload) 73 - if err != nil { 74 - return ContactCard{}, fmt.Errorf("marshal contact card payload: %w", err) 75 - } 76 - 77 - canonicalPayload, err := canonicalizeJCS(payloadRaw) 78 - if err != nil { 79 - return ContactCard{}, WrapProtocolError(ErrInvalidContactCard, "canonicalize payload failed: %v", err) 80 - } 81 - 82 - privKey, err := ParseIdentityPrivateKey(identity.IdentityPrivEd25519) 83 - if err != nil { 84 - return ContactCard{}, err 85 - } 86 - 87 - sigInput := buildContactCardSignInput(canonicalPayload) 88 - sig, err := privKey.Sign(sigInput) 89 - if err != nil { 90 - return ContactCard{}, fmt.Errorf("sign contact card payload: %w", err) 91 - } 92 - 93 - return ContactCard{ 94 - Payload: payload, 95 - SigAlg: ContactCardSigAlgEd25519, 96 - SigFormat: ContactCardSigFormatJCS, 97 - Sig: encodeBase64URL(sig), 98 - }, nil 99 - } 100 - 101 - func ParseAndVerifyContactCard(raw []byte, now time.Time) (ParsedContactCard, error) { 102 - if now.IsZero() { 103 - now = time.Now().UTC() 104 - } 105 - now = now.UTC() 106 - 107 - if err := mustJSONObject(raw); err != nil { 108 - return ParsedContactCard{}, err 109 - } 110 - 111 - var env ContactCardEnvelope 112 - if err := decodeStrictJSON(raw, &env); err != nil { 113 - return ParsedContactCard{}, err 114 - } 115 - 116 - if len(bytes.TrimSpace(env.Payload)) == 0 { 117 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "payload is required") 118 - } 119 - if strings.TrimSpace(env.SigAlg) != ContactCardSigAlgEd25519 { 120 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "sig_alg must be %q", ContactCardSigAlgEd25519) 121 - } 122 - if strings.TrimSpace(env.SigFormat) != ContactCardSigFormatJCS { 123 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "sig_format must be %q", ContactCardSigFormatJCS) 124 - } 125 - if strings.TrimSpace(env.Sig) == "" { 126 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "sig is required") 127 - } 128 - 129 - if err := mustJSONObject(env.Payload); err != nil { 130 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "payload must be JSON object: %v", err) 131 - } 132 - 133 - var payload ContactCardPayload 134 - if err := decodeStrictJSON(env.Payload, &payload); err != nil { 135 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "payload decode failed: %v", err) 136 - } 137 - if err := validateContactCardPayload(payload, now); err != nil { 138 - return ParsedContactCard{}, err 139 - } 140 - 141 - peerID, err := peer.Decode(payload.PeerID) 142 - if err != nil { 143 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "peer_id parse failed: %v", err) 144 - } 145 - 146 - for _, addr := range payload.Addresses { 147 - if err := validateAddressMatchesPeerID(addr, peerID); err != nil { 148 - return ParsedContactCard{}, err 149 - } 150 - } 151 - 152 - canonicalPayload, err := canonicalizeJCS(env.Payload) 153 - if err != nil { 154 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "canonicalize payload failed: %v", err) 155 - } 156 - 157 - sigBytes, err := decodeBase64URL(env.Sig) 158 - if err != nil { 159 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "sig decode failed: %v", err) 160 - } 161 - 162 - pubRaw, err := ParseIdentityPublicKey(payload.IdentityPubEd25519) 163 - if err != nil { 164 - return ParsedContactCard{}, err 165 - } 166 - pub, err := ic.UnmarshalEd25519PublicKey(pubRaw) 167 - if err != nil { 168 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "unmarshal identity_pub_ed25519 failed: %v", err) 169 - } 170 - 171 - verified, err := pub.Verify(buildContactCardSignInput(canonicalPayload), sigBytes) 172 - if err != nil { 173 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "signature verification failed: %v", err) 174 - } 175 - if !verified { 176 - return ParsedContactCard{}, WrapProtocolError(ErrInvalidContactCard, "signature mismatch") 177 - } 178 - 179 - return ParsedContactCard{ 180 - Card: ContactCard{ 181 - Payload: payload, 182 - SigAlg: env.SigAlg, 183 - SigFormat: env.SigFormat, 184 - Sig: env.Sig, 185 - }, 186 - CanonicalPayload: canonicalPayload, 187 - }, nil 188 - } 189 - 190 - func validateContactCardPayload(payload ContactCardPayload, now time.Time) error { 191 - if payload.Version != ContactCardVersionV1 { 192 - return WrapProtocolError(ErrInvalidContactCard, "unsupported payload version: %d", payload.Version) 193 - } 194 - if strings.TrimSpace(payload.NodeUUID) == "" { 195 - return WrapProtocolError(ErrInvalidContactCard, "node_uuid is required") 196 - } 197 - if _, err := uuid.Parse(payload.NodeUUID); err != nil { 198 - return WrapProtocolError(ErrInvalidContactCard, "node_uuid must be a valid UUID: %v", err) 199 - } 200 - if strings.TrimSpace(payload.PeerID) == "" { 201 - return WrapProtocolError(ErrInvalidContactCard, "peer_id is required") 202 - } 203 - if strings.TrimSpace(payload.IdentityPubEd25519) == "" { 204 - return WrapProtocolError(ErrInvalidContactCard, "identity_pub_ed25519 is required") 205 - } 206 - if payload.MinSupportedProtocol <= 0 || payload.MaxSupportedProtocol <= 0 { 207 - return WrapProtocolError(ErrInvalidContactCard, "protocol range must be positive") 208 - } 209 - if payload.MinSupportedProtocol > payload.MaxSupportedProtocol { 210 - return WrapProtocolError(ErrInvalidContactCard, "min_supported_protocol cannot be greater than max_supported_protocol") 211 - } 212 - if payload.IssuedAt.IsZero() { 213 - return WrapProtocolError(ErrInvalidContactCard, "issued_at is required") 214 - } 215 - if payload.ExpiresAt != nil && now.After(payload.ExpiresAt.UTC()) { 216 - return WrapProtocolError(ErrInvalidContactCard, "contact card is expired") 217 - } 218 - 219 - pubPeerID, err := DerivePeerIDFromIdentityPub(payload.IdentityPubEd25519) 220 - if err != nil { 221 - return err 222 - } 223 - expectedPeerID, err := peer.Decode(payload.PeerID) 224 - if err != nil { 225 - return WrapProtocolError(ErrInvalidContactCard, "peer_id decode failed: %v", err) 226 - } 227 - if pubPeerID != expectedPeerID { 228 - return WrapProtocolError(ErrInvalidContactCard, "peer_id does not match identity_pub_ed25519") 229 - } 230 - 231 - if payload.NodeID != "" { 232 - expectedNodeID := NodeIDFromPeerID(expectedPeerID.String()) 233 - if payload.NodeID != expectedNodeID { 234 - return WrapProtocolError(ErrInvalidContactCard, "node_id mismatch, expected %q", expectedNodeID) 235 - } 236 - } 237 - 238 - if len(payload.Addresses) == 0 { 239 - return WrapProtocolError(ErrInvalidContactCard, "addresses cannot be empty") 240 - } 241 - 242 - return nil 243 - } 244 - 245 - func validateAddressMatchesPeerID(rawAddr string, expectedPeerID peer.ID) error { 246 - addr := strings.TrimSpace(rawAddr) 247 - if addr == "" { 248 - return WrapProtocolError(ErrInvalidContactCard, "address is empty") 249 - } 250 - maddr, err := ma.NewMultiaddr(addr) 251 - if err != nil { 252 - return WrapProtocolError(ErrInvalidContactCard, "invalid multiaddr %q: %v", addr, err) 253 - } 254 - if err := validateDialableAddress(maddr, addr); err != nil { 255 - return err 256 - } 257 - _, last := ma.SplitLast(maddr) 258 - if last == nil { 259 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q has no terminal component", addr) 260 - } 261 - if last.Protocol().Code != ma.P_P2P { 262 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q must end with /p2p/<peer_id>", addr) 263 - } 264 - lastPeerID, err := peer.Decode(last.Value()) 265 - if err != nil { 266 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q has invalid /p2p peer id: %v", addr, err) 267 - } 268 - if lastPeerID != expectedPeerID { 269 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q terminal peer id mismatch", addr) 270 - } 271 - return nil 272 - } 273 - 274 - func validateDialableAddress(maddr ma.Multiaddr, rawAddr string) error { 275 - if value, err := maddr.ValueForProtocol(ma.P_IP4); err == nil { 276 - if ip := net.ParseIP(strings.TrimSpace(value)); ip != nil && ip.IsUnspecified() { 277 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q is non-dialable: ip4 unspecified", rawAddr) 278 - } 279 - } 280 - if value, err := maddr.ValueForProtocol(ma.P_IP6); err == nil { 281 - if ip := net.ParseIP(strings.TrimSpace(value)); ip != nil && ip.IsUnspecified() { 282 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q is non-dialable: ip6 unspecified", rawAddr) 283 - } 284 - } 285 - if value, err := maddr.ValueForProtocol(ma.P_TCP); err == nil && strings.TrimSpace(value) == "0" { 286 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q is non-dialable: tcp port 0", rawAddr) 287 - } 288 - if value, err := maddr.ValueForProtocol(ma.P_UDP); err == nil && strings.TrimSpace(value) == "0" { 289 - return WrapProtocolError(ErrInvalidContactCard, "multiaddr %q is non-dialable: udp port 0", rawAddr) 290 - } 291 - return nil 292 - } 293 - 294 - func normalizeAddresses(addresses []string) []string { 295 - if len(addresses) == 0 { 296 - return nil 297 - } 298 - out := make([]string, 0, len(addresses)) 299 - seen := map[string]struct{}{} 300 - for _, raw := range addresses { 301 - addr := strings.TrimSpace(raw) 302 - if addr == "" { 303 - continue 304 - } 305 - if _, ok := seen[addr]; ok { 306 - continue 307 - } 308 - seen[addr] = struct{}{} 309 - out = append(out, addr) 310 - } 311 - return out 312 - } 313 - 314 - func canonicalizeJCS(rawJSON []byte) ([]byte, error) { 315 - return jsoncanonicalizer.Transform(rawJSON) 316 - } 317 - 318 - func buildContactCardSignInput(canonicalPayload []byte) []byte { 319 - buf := make([]byte, 0, len(ContactCardSignDomainV1)+len(canonicalPayload)) 320 - buf = append(buf, ContactCardSignDomainV1...) 321 - buf = append(buf, canonicalPayload...) 322 - return buf 323 - }
-143
maep/contact_card_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "strings" 7 - "testing" 8 - "time" 9 - ) 10 - 11 - func TestBuildAndVerifyContactCard(t *testing.T) { 12 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 13 - identity, err := GenerateIdentity(now) 14 - if err != nil { 15 - t.Fatalf("GenerateIdentity() error = %v", err) 16 - } 17 - 18 - addr := fmt.Sprintf("/dns4/example.com/udp/4001/quic-v1/p2p/%s", identity.PeerID) 19 - card, err := BuildSignedContactCard(identity, []string{addr}, 1, 1, now, nil) 20 - if err != nil { 21 - t.Fatalf("BuildSignedContactCard() error = %v", err) 22 - } 23 - 24 - raw, err := json.Marshal(card) 25 - if err != nil { 26 - t.Fatalf("json.Marshal(card) error = %v", err) 27 - } 28 - 29 - parsed, err := ParseAndVerifyContactCard(raw, now) 30 - if err != nil { 31 - t.Fatalf("ParseAndVerifyContactCard() error = %v", err) 32 - } 33 - if parsed.Card.Payload.PeerID != identity.PeerID { 34 - t.Fatalf("peer_id mismatch: got %s want %s", parsed.Card.Payload.PeerID, identity.PeerID) 35 - } 36 - } 37 - 38 - func TestVerifyContactCardDetectsTamper(t *testing.T) { 39 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 40 - identity, err := GenerateIdentity(now) 41 - if err != nil { 42 - t.Fatalf("GenerateIdentity() error = %v", err) 43 - } 44 - 45 - addr := fmt.Sprintf("/dns4/example.com/udp/4001/quic-v1/p2p/%s", identity.PeerID) 46 - card, err := BuildSignedContactCard(identity, []string{addr}, 1, 1, now, nil) 47 - if err != nil { 48 - t.Fatalf("BuildSignedContactCard() error = %v", err) 49 - } 50 - 51 - raw, err := json.Marshal(card) 52 - if err != nil { 53 - t.Fatalf("json.Marshal(card) error = %v", err) 54 - } 55 - 56 - var tampered map[string]any 57 - if err := json.Unmarshal(raw, &tampered); err != nil { 58 - t.Fatalf("json.Unmarshal(raw) error = %v", err) 59 - } 60 - payload, ok := tampered["payload"].(map[string]any) 61 - if !ok { 62 - t.Fatalf("payload type assertion failed") 63 - } 64 - payload["node_uuid"] = "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f999" 65 - tamperedRaw, err := json.Marshal(tampered) 66 - if err != nil { 67 - t.Fatalf("json.Marshal(tampered) error = %v", err) 68 - } 69 - 70 - if _, err := ParseAndVerifyContactCard(tamperedRaw, now); err == nil { 71 - t.Fatalf("expected signature verification error, got nil") 72 - } 73 - } 74 - 75 - func TestBuildSignedContactCard_RejectsNonDialableAddress(t *testing.T) { 76 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 77 - identity, err := GenerateIdentity(now) 78 - if err != nil { 79 - t.Fatalf("GenerateIdentity() error = %v", err) 80 - } 81 - 82 - addr := fmt.Sprintf("/ip4/0.0.0.0/tcp/4021/p2p/%s", identity.PeerID) 83 - _, err = BuildSignedContactCard(identity, []string{addr}, 1, 1, now, nil) 84 - if err == nil { 85 - t.Fatalf("expected non-dialable address error, got nil") 86 - } 87 - if !strings.Contains(err.Error(), "non-dialable") { 88 - t.Fatalf("unexpected error: %v", err) 89 - } 90 - } 91 - 92 - func TestParseAndVerifyContactCard_RejectsNonDialableAddress(t *testing.T) { 93 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 94 - identity, err := GenerateIdentity(now) 95 - if err != nil { 96 - t.Fatalf("GenerateIdentity() error = %v", err) 97 - } 98 - 99 - payload := ContactCardPayload{ 100 - Version: ContactCardVersionV1, 101 - NodeUUID: identity.NodeUUID, 102 - PeerID: identity.PeerID, 103 - NodeID: NodeIDFromPeerID(identity.PeerID), 104 - IdentityPubEd25519: identity.IdentityPubEd25519, 105 - Addresses: []string{fmt.Sprintf("/ip4/0.0.0.0/tcp/4021/p2p/%s", identity.PeerID)}, 106 - MinSupportedProtocol: 1, 107 - MaxSupportedProtocol: 1, 108 - IssuedAt: now, 109 - } 110 - payloadRaw, err := json.Marshal(payload) 111 - if err != nil { 112 - t.Fatalf("json.Marshal(payload) error = %v", err) 113 - } 114 - canonicalPayload, err := canonicalizeJCS(payloadRaw) 115 - if err != nil { 116 - t.Fatalf("canonicalizeJCS(payload) error = %v", err) 117 - } 118 - priv, err := ParseIdentityPrivateKey(identity.IdentityPrivEd25519) 119 - if err != nil { 120 - t.Fatalf("ParseIdentityPrivateKey() error = %v", err) 121 - } 122 - sig, err := priv.Sign(buildContactCardSignInput(canonicalPayload)) 123 - if err != nil { 124 - t.Fatalf("Sign() error = %v", err) 125 - } 126 - rawCard, err := json.Marshal(ContactCardEnvelope{ 127 - Payload: payloadRaw, 128 - SigAlg: ContactCardSigAlgEd25519, 129 - SigFormat: ContactCardSigFormatJCS, 130 - Sig: encodeBase64URL(sig), 131 - }) 132 - if err != nil { 133 - t.Fatalf("json.Marshal(card) error = %v", err) 134 - } 135 - 136 - _, err = ParseAndVerifyContactCard(rawCard, now) 137 - if err == nil { 138 - t.Fatalf("expected non-dialable address error, got nil") 139 - } 140 - if !strings.Contains(err.Error(), "non-dialable") { 141 - t.Fatalf("unexpected error: %v", err) 142 - } 143 - }
-71
maep/errors.go
··· 1 - package maep 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - ) 7 - 8 - const ( 9 - ErrUnauthorizedSymbol = "ERR_UNAUTHORIZED" 10 - ErrPeerIDMismatchSymbol = "ERR_PEER_ID_MISMATCH" 11 - ErrContactConflictedSymbol = "ERR_CONTACT_CONFLICTED" 12 - ErrMethodNotAllowedSymbol = "ERR_METHOD_NOT_ALLOWED" 13 - ErrPayloadTooLargeSymbol = "ERR_PAYLOAD_TOO_LARGE" 14 - ErrRateLimitedSymbol = "ERR_RATE_LIMITED" 15 - ErrUnsupportedProtocolSymbol = "ERR_UNSUPPORTED_PROTOCOL" 16 - ErrInvalidJSONProfileSymbol = "ERR_INVALID_JSON_PROFILE" 17 - ErrInvalidContactCardSymbol = "ERR_INVALID_CONTACT_CARD" 18 - ErrInvalidParamsSymbol = "ERR_INVALID_PARAMS" 19 - ) 20 - 21 - type ProtocolError struct { 22 - Symbol string 23 - Message string 24 - } 25 - 26 - func (e *ProtocolError) Error() string { 27 - if e == nil { 28 - return "" 29 - } 30 - if e.Message == "" { 31 - return e.Symbol 32 - } 33 - return fmt.Sprintf("%s: %s", e.Symbol, e.Message) 34 - } 35 - 36 - func (e *ProtocolError) Is(target error) bool { 37 - t, ok := target.(*ProtocolError) 38 - if !ok { 39 - return false 40 - } 41 - return e.Symbol == t.Symbol 42 - } 43 - 44 - var ( 45 - ErrUnauthorized = &ProtocolError{Symbol: ErrUnauthorizedSymbol, Message: "unauthorized"} 46 - ErrPeerIDMismatch = &ProtocolError{Symbol: ErrPeerIDMismatchSymbol, Message: "peer id mismatch"} 47 - ErrInvalidJSONProfile = &ProtocolError{Symbol: ErrInvalidJSONProfileSymbol, Message: "invalid JSON profile"} 48 - ErrInvalidContactCard = &ProtocolError{Symbol: ErrInvalidContactCardSymbol, Message: "invalid contact card"} 49 - ErrContactConflicted = &ProtocolError{Symbol: ErrContactConflictedSymbol, Message: "contact conflicted"} 50 - ErrMethodNotAllowed = &ProtocolError{Symbol: ErrMethodNotAllowedSymbol, Message: "method not allowed"} 51 - ErrPayloadTooLarge = &ProtocolError{Symbol: ErrPayloadTooLargeSymbol, Message: "payload too large"} 52 - ErrRateLimited = &ProtocolError{Symbol: ErrRateLimitedSymbol, Message: "rate limited"} 53 - ErrUnsupportedProtocol = &ProtocolError{Symbol: ErrUnsupportedProtocolSymbol, Message: "unsupported protocol"} 54 - ErrInvalidParams = &ProtocolError{Symbol: ErrInvalidParamsSymbol, Message: "invalid params"} 55 - ) 56 - 57 - func WrapProtocolError(base *ProtocolError, format string, args ...any) error { 58 - if base == nil { 59 - return fmt.Errorf(format, args...) 60 - } 61 - msg := fmt.Sprintf(format, args...) 62 - return &ProtocolError{Symbol: base.Symbol, Message: msg} 63 - } 64 - 65 - func SymbolOf(err error) string { 66 - var pe *ProtocolError 67 - if errors.As(err, &pe) { 68 - return pe.Symbol 69 - } 70 - return "" 71 - }
-897
maep/file_store.go
··· 1 - package maep 2 - 3 - import ( 4 - "bufio" 5 - "bytes" 6 - "context" 7 - "encoding/json" 8 - "fmt" 9 - "os" 10 - "path/filepath" 11 - "sort" 12 - "strings" 13 - "sync" 14 - "time" 15 - 16 - "github.com/quailyquaily/mistermorph/internal/fsstore" 17 - ) 18 - 19 - const ( 20 - contactsFileVersion = 1 21 - dedupeFileVersion = 1 22 - protocolHistoryFileVersion = 1 23 - ) 24 - 25 - type FileStore struct { 26 - root string 27 - 28 - mu sync.Mutex 29 - } 30 - 31 - type contactsFile struct { 32 - Version int `json:"version"` 33 - Contacts []Contact `json:"contacts"` 34 - } 35 - 36 - type dedupeFile struct { 37 - Version int `json:"version"` 38 - Records []DedupeRecord `json:"records"` 39 - } 40 - 41 - type protocolHistoryFile struct { 42 - Version int `json:"version"` 43 - Records []ProtocolHistory `json:"records"` 44 - } 45 - 46 - func NewFileStore(root string) *FileStore { 47 - return &FileStore{root: strings.TrimSpace(root)} 48 - } 49 - 50 - func (s *FileStore) Ensure(ctx context.Context) error { 51 - if ctx != nil { 52 - select { 53 - case <-ctx.Done(): 54 - return ctx.Err() 55 - default: 56 - } 57 - } 58 - s.mu.Lock() 59 - defer s.mu.Unlock() 60 - return fsstore.EnsureDir(s.rootPath(), 0o700) 61 - } 62 - 63 - func (s *FileStore) GetIdentity(ctx context.Context) (Identity, bool, error) { 64 - if err := s.ensureNotCanceled(ctx); err != nil { 65 - return Identity{}, false, err 66 - } 67 - s.mu.Lock() 68 - defer s.mu.Unlock() 69 - 70 - var identity Identity 71 - ok, err := s.readJSONFile(s.identityPath(), &identity) 72 - if err != nil { 73 - return Identity{}, false, err 74 - } 75 - if !ok { 76 - return Identity{}, false, nil 77 - } 78 - return identity, true, nil 79 - } 80 - 81 - func (s *FileStore) PutIdentity(ctx context.Context, identity Identity) error { 82 - if err := s.ensureNotCanceled(ctx); err != nil { 83 - return err 84 - } 85 - s.mu.Lock() 86 - defer s.mu.Unlock() 87 - return s.withStateLock(ctx, func() error { 88 - return s.writeJSONFileAtomic(s.identityPath(), identity, 0o600) 89 - }) 90 - } 91 - 92 - func (s *FileStore) GetContactByPeerID(ctx context.Context, peerID string) (Contact, bool, error) { 93 - if err := s.ensureNotCanceled(ctx); err != nil { 94 - return Contact{}, false, err 95 - } 96 - s.mu.Lock() 97 - defer s.mu.Unlock() 98 - 99 - contacts, err := s.loadContactsLocked() 100 - if err != nil { 101 - return Contact{}, false, err 102 - } 103 - peerID = strings.TrimSpace(peerID) 104 - for _, contact := range contacts { 105 - if strings.TrimSpace(contact.PeerID) == peerID { 106 - return contact, true, nil 107 - } 108 - } 109 - return Contact{}, false, nil 110 - } 111 - 112 - func (s *FileStore) GetContactByNodeUUID(ctx context.Context, nodeUUID string) (Contact, bool, error) { 113 - if err := s.ensureNotCanceled(ctx); err != nil { 114 - return Contact{}, false, err 115 - } 116 - s.mu.Lock() 117 - defer s.mu.Unlock() 118 - 119 - contacts, err := s.loadContactsLocked() 120 - if err != nil { 121 - return Contact{}, false, err 122 - } 123 - nodeUUID = strings.TrimSpace(nodeUUID) 124 - for _, contact := range contacts { 125 - if strings.TrimSpace(contact.NodeUUID) == nodeUUID { 126 - return contact, true, nil 127 - } 128 - } 129 - return Contact{}, false, nil 130 - } 131 - 132 - func (s *FileStore) PutContact(ctx context.Context, contact Contact) error { 133 - if err := s.ensureNotCanceled(ctx); err != nil { 134 - return err 135 - } 136 - s.mu.Lock() 137 - defer s.mu.Unlock() 138 - return s.withStateLock(ctx, func() error { 139 - contacts, err := s.loadContactsLocked() 140 - if err != nil { 141 - return err 142 - } 143 - 144 - replaced := false 145 - for i := range contacts { 146 - if strings.TrimSpace(contacts[i].PeerID) == strings.TrimSpace(contact.PeerID) { 147 - if contacts[i].CreatedAt.IsZero() { 148 - contacts[i].CreatedAt = contact.CreatedAt 149 - } 150 - contact.CreatedAt = contacts[i].CreatedAt 151 - contacts[i] = contact 152 - replaced = true 153 - break 154 - } 155 - } 156 - if !replaced { 157 - contacts = append(contacts, contact) 158 - } 159 - 160 - return s.saveContactsLocked(contacts) 161 - }) 162 - } 163 - 164 - func (s *FileStore) ListContacts(ctx context.Context) ([]Contact, error) { 165 - if err := s.ensureNotCanceled(ctx); err != nil { 166 - return nil, err 167 - } 168 - s.mu.Lock() 169 - defer s.mu.Unlock() 170 - 171 - contacts, err := s.loadContactsLocked() 172 - if err != nil { 173 - return nil, err 174 - } 175 - out := make([]Contact, len(contacts)) 176 - copy(out, contacts) 177 - return out, nil 178 - } 179 - 180 - func (s *FileStore) AppendAuditEvent(ctx context.Context, event AuditEvent) error { 181 - if err := s.ensureNotCanceled(ctx); err != nil { 182 - return err 183 - } 184 - s.mu.Lock() 185 - defer s.mu.Unlock() 186 - 187 - event.EventID = strings.TrimSpace(event.EventID) 188 - event.Action = strings.TrimSpace(event.Action) 189 - event.PeerID = strings.TrimSpace(event.PeerID) 190 - event.NodeUUID = strings.TrimSpace(event.NodeUUID) 191 - event.Reason = strings.TrimSpace(event.Reason) 192 - if event.CreatedAt.IsZero() { 193 - event.CreatedAt = time.Now().UTC() 194 - } 195 - if event.Metadata != nil && len(event.Metadata) == 0 { 196 - event.Metadata = nil 197 - } 198 - return s.withAuditLock(ctx, func() error { 199 - return s.appendAuditEventLocked(event) 200 - }) 201 - } 202 - 203 - func (s *FileStore) ListAuditEvents(ctx context.Context, peerID string, action string, limit int) ([]AuditEvent, error) { 204 - if err := s.ensureNotCanceled(ctx); err != nil { 205 - return nil, err 206 - } 207 - s.mu.Lock() 208 - defer s.mu.Unlock() 209 - peerID = strings.TrimSpace(peerID) 210 - action = strings.TrimSpace(action) 211 - 212 - var out []AuditEvent 213 - err := s.withAuditLock(ctx, func() error { 214 - records, err := s.loadAuditEventsLocked() 215 - if err != nil { 216 - return err 217 - } 218 - 219 - filtered := make([]AuditEvent, 0, len(records)) 220 - for _, record := range records { 221 - if peerID != "" && strings.TrimSpace(record.PeerID) != peerID { 222 - continue 223 - } 224 - if action != "" && strings.TrimSpace(record.Action) != action { 225 - continue 226 - } 227 - filtered = append(filtered, record) 228 - } 229 - sort.Slice(filtered, func(i, j int) bool { 230 - if filtered[i].CreatedAt.Equal(filtered[j].CreatedAt) { 231 - return strings.TrimSpace(filtered[i].EventID) > strings.TrimSpace(filtered[j].EventID) 232 - } 233 - return filtered[i].CreatedAt.After(filtered[j].CreatedAt) 234 - }) 235 - if limit > 0 && len(filtered) > limit { 236 - filtered = filtered[:limit] 237 - } 238 - out = make([]AuditEvent, len(filtered)) 239 - copy(out, filtered) 240 - return nil 241 - }) 242 - return out, err 243 - } 244 - 245 - func (s *FileStore) AppendInboxMessage(ctx context.Context, message InboxMessage) error { 246 - if err := s.ensureNotCanceled(ctx); err != nil { 247 - return err 248 - } 249 - s.mu.Lock() 250 - defer s.mu.Unlock() 251 - return s.withStateLock(ctx, func() error { 252 - message.MessageID = strings.TrimSpace(message.MessageID) 253 - message.FromPeerID = strings.TrimSpace(message.FromPeerID) 254 - message.Topic = strings.TrimSpace(message.Topic) 255 - message.ContentType = strings.TrimSpace(message.ContentType) 256 - message.PayloadBase64 = strings.TrimSpace(message.PayloadBase64) 257 - message.IdempotencyKey = strings.TrimSpace(message.IdempotencyKey) 258 - message.SessionID = strings.TrimSpace(message.SessionID) 259 - message.ReplyTo = strings.TrimSpace(message.ReplyTo) 260 - if message.ReceivedAt.IsZero() { 261 - message.ReceivedAt = time.Now().UTC() 262 - } 263 - return s.appendInboxMessageLocked(message) 264 - }) 265 - } 266 - 267 - func (s *FileStore) ListInboxMessages(ctx context.Context, fromPeerID string, topic string, limit int) ([]InboxMessage, error) { 268 - if err := s.ensureNotCanceled(ctx); err != nil { 269 - return nil, err 270 - } 271 - s.mu.Lock() 272 - defer s.mu.Unlock() 273 - 274 - records, err := s.loadInboxMessagesLocked() 275 - if err != nil { 276 - return nil, err 277 - } 278 - fromPeerID = strings.TrimSpace(fromPeerID) 279 - topic = strings.TrimSpace(topic) 280 - 281 - filtered := make([]InboxMessage, 0, len(records)) 282 - for _, record := range records { 283 - if fromPeerID != "" && strings.TrimSpace(record.FromPeerID) != fromPeerID { 284 - continue 285 - } 286 - if topic != "" && strings.TrimSpace(record.Topic) != topic { 287 - continue 288 - } 289 - filtered = append(filtered, record) 290 - } 291 - sort.Slice(filtered, func(i, j int) bool { 292 - if filtered[i].ReceivedAt.Equal(filtered[j].ReceivedAt) { 293 - return strings.TrimSpace(filtered[i].MessageID) > strings.TrimSpace(filtered[j].MessageID) 294 - } 295 - return filtered[i].ReceivedAt.After(filtered[j].ReceivedAt) 296 - }) 297 - 298 - if limit > 0 && len(filtered) > limit { 299 - filtered = filtered[:limit] 300 - } 301 - out := make([]InboxMessage, len(filtered)) 302 - copy(out, filtered) 303 - return out, nil 304 - } 305 - 306 - func (s *FileStore) AppendOutboxMessage(ctx context.Context, message OutboxMessage) error { 307 - if err := s.ensureNotCanceled(ctx); err != nil { 308 - return err 309 - } 310 - s.mu.Lock() 311 - defer s.mu.Unlock() 312 - return s.withStateLock(ctx, func() error { 313 - message.MessageID = strings.TrimSpace(message.MessageID) 314 - message.ToPeerID = strings.TrimSpace(message.ToPeerID) 315 - message.Topic = strings.TrimSpace(message.Topic) 316 - message.ContentType = strings.TrimSpace(message.ContentType) 317 - message.PayloadBase64 = strings.TrimSpace(message.PayloadBase64) 318 - message.IdempotencyKey = strings.TrimSpace(message.IdempotencyKey) 319 - message.SessionID = strings.TrimSpace(message.SessionID) 320 - message.ReplyTo = strings.TrimSpace(message.ReplyTo) 321 - if message.SentAt.IsZero() { 322 - message.SentAt = time.Now().UTC() 323 - } 324 - return s.appendOutboxMessageLocked(message) 325 - }) 326 - } 327 - 328 - func (s *FileStore) ListOutboxMessages(ctx context.Context, toPeerID string, topic string, limit int) ([]OutboxMessage, error) { 329 - if err := s.ensureNotCanceled(ctx); err != nil { 330 - return nil, err 331 - } 332 - s.mu.Lock() 333 - defer s.mu.Unlock() 334 - 335 - records, err := s.loadOutboxMessagesLocked() 336 - if err != nil { 337 - return nil, err 338 - } 339 - toPeerID = strings.TrimSpace(toPeerID) 340 - topic = strings.TrimSpace(topic) 341 - 342 - filtered := make([]OutboxMessage, 0, len(records)) 343 - for _, record := range records { 344 - if toPeerID != "" && strings.TrimSpace(record.ToPeerID) != toPeerID { 345 - continue 346 - } 347 - if topic != "" && strings.TrimSpace(record.Topic) != topic { 348 - continue 349 - } 350 - filtered = append(filtered, record) 351 - } 352 - sort.Slice(filtered, func(i, j int) bool { 353 - if filtered[i].SentAt.Equal(filtered[j].SentAt) { 354 - return strings.TrimSpace(filtered[i].MessageID) > strings.TrimSpace(filtered[j].MessageID) 355 - } 356 - return filtered[i].SentAt.After(filtered[j].SentAt) 357 - }) 358 - 359 - if limit > 0 && len(filtered) > limit { 360 - filtered = filtered[:limit] 361 - } 362 - out := make([]OutboxMessage, len(filtered)) 363 - copy(out, filtered) 364 - return out, nil 365 - } 366 - 367 - func (s *FileStore) GetDedupeRecord(ctx context.Context, fromPeerID string, topic string, idempotencyKey string) (DedupeRecord, bool, error) { 368 - if err := s.ensureNotCanceled(ctx); err != nil { 369 - return DedupeRecord{}, false, err 370 - } 371 - s.mu.Lock() 372 - defer s.mu.Unlock() 373 - 374 - records, err := s.loadDedupeRecordsLocked() 375 - if err != nil { 376 - return DedupeRecord{}, false, err 377 - } 378 - fromPeerID = strings.TrimSpace(fromPeerID) 379 - topic = strings.TrimSpace(topic) 380 - idempotencyKey = strings.TrimSpace(idempotencyKey) 381 - now := time.Now().UTC() 382 - for _, record := range records { 383 - if strings.TrimSpace(record.FromPeerID) != fromPeerID { 384 - continue 385 - } 386 - if strings.TrimSpace(record.Topic) != topic { 387 - continue 388 - } 389 - if strings.TrimSpace(record.IdempotencyKey) != idempotencyKey { 390 - continue 391 - } 392 - if !record.ExpiresAt.IsZero() && !record.ExpiresAt.After(now) { 393 - continue 394 - } 395 - return record, true, nil 396 - } 397 - return DedupeRecord{}, false, nil 398 - } 399 - 400 - func (s *FileStore) PutDedupeRecord(ctx context.Context, record DedupeRecord) error { 401 - if err := s.ensureNotCanceled(ctx); err != nil { 402 - return err 403 - } 404 - s.mu.Lock() 405 - defer s.mu.Unlock() 406 - return s.withStateLock(ctx, func() error { 407 - records, err := s.loadDedupeRecordsLocked() 408 - if err != nil { 409 - return err 410 - } 411 - 412 - now := time.Now().UTC() 413 - record.FromPeerID = strings.TrimSpace(record.FromPeerID) 414 - record.Topic = strings.TrimSpace(record.Topic) 415 - record.IdempotencyKey = strings.TrimSpace(record.IdempotencyKey) 416 - if record.CreatedAt.IsZero() { 417 - record.CreatedAt = now 418 - } 419 - if record.ExpiresAt.IsZero() { 420 - record.ExpiresAt = record.CreatedAt.Add(DefaultDedupeTTL) 421 - } 422 - 423 - replaced := false 424 - for i := range records { 425 - if strings.TrimSpace(records[i].FromPeerID) != record.FromPeerID { 426 - continue 427 - } 428 - if strings.TrimSpace(records[i].Topic) != record.Topic { 429 - continue 430 - } 431 - if strings.TrimSpace(records[i].IdempotencyKey) != record.IdempotencyKey { 432 - continue 433 - } 434 - records[i] = record 435 - replaced = true 436 - break 437 - } 438 - if !replaced { 439 - records = append(records, record) 440 - } 441 - 442 - return s.saveDedupeRecordsLocked(records) 443 - }) 444 - } 445 - 446 - func (s *FileStore) PruneDedupeRecords(ctx context.Context, now time.Time, maxEntries int) (int, error) { 447 - if err := s.ensureNotCanceled(ctx); err != nil { 448 - return 0, err 449 - } 450 - if now.IsZero() { 451 - now = time.Now().UTC() 452 - } 453 - if maxEntries <= 0 { 454 - maxEntries = DefaultDedupeMaxEntries 455 - } 456 - 457 - s.mu.Lock() 458 - defer s.mu.Unlock() 459 - removed := 0 460 - err := s.withStateLock(ctx, func() error { 461 - records, err := s.loadDedupeRecordsLocked() 462 - if err != nil { 463 - return err 464 - } 465 - if len(records) == 0 { 466 - removed = 0 467 - return nil 468 - } 469 - 470 - active := make([]DedupeRecord, 0, len(records)) 471 - for _, record := range records { 472 - if !record.ExpiresAt.IsZero() && !record.ExpiresAt.After(now) { 473 - continue 474 - } 475 - active = append(active, record) 476 - } 477 - 478 - sort.Slice(active, func(i, j int) bool { 479 - if active[i].CreatedAt.Equal(active[j].CreatedAt) { 480 - leftPeer := strings.TrimSpace(active[i].FromPeerID) 481 - rightPeer := strings.TrimSpace(active[j].FromPeerID) 482 - if leftPeer != rightPeer { 483 - return leftPeer < rightPeer 484 - } 485 - leftTopic := strings.TrimSpace(active[i].Topic) 486 - rightTopic := strings.TrimSpace(active[j].Topic) 487 - if leftTopic != rightTopic { 488 - return leftTopic < rightTopic 489 - } 490 - return strings.TrimSpace(active[i].IdempotencyKey) < strings.TrimSpace(active[j].IdempotencyKey) 491 - } 492 - return active[i].CreatedAt.After(active[j].CreatedAt) 493 - }) 494 - 495 - kept := active 496 - if len(kept) > maxEntries { 497 - kept = kept[:maxEntries] 498 - } 499 - 500 - removed = len(records) - len(kept) 501 - if removed <= 0 { 502 - return nil 503 - } 504 - return s.saveDedupeRecordsLocked(kept) 505 - }) 506 - if err != nil { 507 - return 0, err 508 - } 509 - return removed, nil 510 - } 511 - 512 - func (s *FileStore) GetProtocolHistory(ctx context.Context, peerID string) (ProtocolHistory, bool, error) { 513 - if err := s.ensureNotCanceled(ctx); err != nil { 514 - return ProtocolHistory{}, false, err 515 - } 516 - s.mu.Lock() 517 - defer s.mu.Unlock() 518 - 519 - history, err := s.loadProtocolHistoryLocked() 520 - if err != nil { 521 - return ProtocolHistory{}, false, err 522 - } 523 - peerID = strings.TrimSpace(peerID) 524 - for _, record := range history { 525 - if strings.TrimSpace(record.PeerID) == peerID { 526 - return record, true, nil 527 - } 528 - } 529 - return ProtocolHistory{}, false, nil 530 - } 531 - 532 - func (s *FileStore) PutProtocolHistory(ctx context.Context, history ProtocolHistory) error { 533 - if err := s.ensureNotCanceled(ctx); err != nil { 534 - return err 535 - } 536 - s.mu.Lock() 537 - defer s.mu.Unlock() 538 - return s.withStateLock(ctx, func() error { 539 - records, err := s.loadProtocolHistoryLocked() 540 - if err != nil { 541 - return err 542 - } 543 - 544 - history.PeerID = strings.TrimSpace(history.PeerID) 545 - if history.UpdatedAt.IsZero() { 546 - history.UpdatedAt = time.Now().UTC() 547 - } 548 - replaced := false 549 - for i := range records { 550 - if strings.TrimSpace(records[i].PeerID) == history.PeerID { 551 - records[i] = history 552 - replaced = true 553 - break 554 - } 555 - } 556 - if !replaced { 557 - records = append(records, history) 558 - } 559 - return s.saveProtocolHistoryLocked(records) 560 - }) 561 - } 562 - 563 - func (s *FileStore) loadContactsLocked() ([]Contact, error) { 564 - var file contactsFile 565 - ok, err := s.readJSONFile(s.contactsPath(), &file) 566 - if err != nil { 567 - return nil, err 568 - } 569 - if !ok { 570 - return []Contact{}, nil 571 - } 572 - out := make([]Contact, 0, len(file.Contacts)) 573 - for _, c := range file.Contacts { 574 - out = append(out, c) 575 - } 576 - return out, nil 577 - } 578 - 579 - func (s *FileStore) saveContactsLocked(contacts []Contact) error { 580 - sort.Slice(contacts, func(i, j int) bool { 581 - left := strings.TrimSpace(contacts[i].PeerID) 582 - right := strings.TrimSpace(contacts[j].PeerID) 583 - if left == right { 584 - return contacts[i].UpdatedAt.Before(contacts[j].UpdatedAt) 585 - } 586 - return left < right 587 - }) 588 - 589 - file := contactsFile{ 590 - Version: contactsFileVersion, 591 - Contacts: contacts, 592 - } 593 - return s.writeJSONFileAtomic(s.contactsPath(), file, 0o600) 594 - } 595 - 596 - func (s *FileStore) loadAuditEventsLocked() ([]AuditEvent, error) { 597 - records, ok, err := s.readAuditEventsJSONL(s.auditPathJSONL()) 598 - if err != nil { 599 - return nil, err 600 - } 601 - if ok { 602 - return records, nil 603 - } 604 - return []AuditEvent{}, nil 605 - } 606 - 607 - func (s *FileStore) loadDedupeRecordsLocked() ([]DedupeRecord, error) { 608 - var file dedupeFile 609 - ok, err := s.readJSONFile(s.dedupePath(), &file) 610 - if err != nil { 611 - return nil, err 612 - } 613 - if !ok { 614 - return []DedupeRecord{}, nil 615 - } 616 - out := make([]DedupeRecord, 0, len(file.Records)) 617 - for _, record := range file.Records { 618 - out = append(out, record) 619 - } 620 - return out, nil 621 - } 622 - 623 - func (s *FileStore) saveDedupeRecordsLocked(records []DedupeRecord) error { 624 - file := dedupeFile{Version: dedupeFileVersion, Records: records} 625 - return s.writeJSONFileAtomic(s.dedupePath(), file, 0o600) 626 - } 627 - 628 - func (s *FileStore) loadInboxMessagesLocked() ([]InboxMessage, error) { 629 - records, ok, err := s.readInboxMessagesJSONL(s.inboxPathJSONL()) 630 - if err != nil { 631 - return nil, err 632 - } 633 - if !ok { 634 - return []InboxMessage{}, nil 635 - } 636 - return records, nil 637 - } 638 - 639 - func (s *FileStore) appendInboxMessageLocked(message InboxMessage) error { 640 - writer, err := fsstore.NewJSONLWriter(s.inboxPathJSONL(), fsstore.JSONLOptions{ 641 - DirPerm: 0o700, 642 - FilePerm: 0o600, 643 - FlushEachWrite: true, 644 - }) 645 - if err != nil { 646 - return fmt.Errorf("open inbox writer: %w", err) 647 - } 648 - defer writer.Close() 649 - if err := writer.AppendJSON(message); err != nil { 650 - return fmt.Errorf("append inbox message: %w", err) 651 - } 652 - return nil 653 - } 654 - 655 - func (s *FileStore) loadOutboxMessagesLocked() ([]OutboxMessage, error) { 656 - records, ok, err := s.readOutboxMessagesJSONL(s.outboxPathJSONL()) 657 - if err != nil { 658 - return nil, err 659 - } 660 - if !ok { 661 - return []OutboxMessage{}, nil 662 - } 663 - return records, nil 664 - } 665 - 666 - func (s *FileStore) appendOutboxMessageLocked(message OutboxMessage) error { 667 - writer, err := fsstore.NewJSONLWriter(s.outboxPathJSONL(), fsstore.JSONLOptions{ 668 - DirPerm: 0o700, 669 - FilePerm: 0o600, 670 - FlushEachWrite: true, 671 - }) 672 - if err != nil { 673 - return fmt.Errorf("open outbox writer: %w", err) 674 - } 675 - defer writer.Close() 676 - if err := writer.AppendJSON(message); err != nil { 677 - return fmt.Errorf("append outbox message: %w", err) 678 - } 679 - return nil 680 - } 681 - 682 - func (s *FileStore) loadProtocolHistoryLocked() ([]ProtocolHistory, error) { 683 - var file protocolHistoryFile 684 - ok, err := s.readJSONFile(s.protocolHistoryPath(), &file) 685 - if err != nil { 686 - return nil, err 687 - } 688 - if !ok { 689 - return []ProtocolHistory{}, nil 690 - } 691 - out := make([]ProtocolHistory, 0, len(file.Records)) 692 - for _, record := range file.Records { 693 - out = append(out, record) 694 - } 695 - return out, nil 696 - } 697 - 698 - func (s *FileStore) saveProtocolHistoryLocked(records []ProtocolHistory) error { 699 - sort.Slice(records, func(i, j int) bool { 700 - return strings.TrimSpace(records[i].PeerID) < strings.TrimSpace(records[j].PeerID) 701 - }) 702 - file := protocolHistoryFile{Version: protocolHistoryFileVersion, Records: records} 703 - return s.writeJSONFileAtomic(s.protocolHistoryPath(), file, 0o600) 704 - } 705 - 706 - func (s *FileStore) readJSONFile(path string, out any) (bool, error) { 707 - ok, err := fsstore.ReadJSON(path, out) 708 - if err != nil { 709 - return false, fmt.Errorf("read %s: %w", path, err) 710 - } 711 - return ok, nil 712 - } 713 - 714 - func (s *FileStore) writeJSONFileAtomic(path string, v any, perm os.FileMode) error { 715 - return fsstore.WriteJSONAtomic(path, v, fsstore.FileOptions{ 716 - DirPerm: 0o700, 717 - FilePerm: perm, 718 - }) 719 - } 720 - 721 - func (s *FileStore) appendAuditEventLocked(event AuditEvent) error { 722 - writer, err := fsstore.NewJSONLWriter(s.auditPathJSONL(), fsstore.JSONLOptions{ 723 - DirPerm: 0o700, 724 - FilePerm: 0o600, 725 - FlushEachWrite: true, 726 - }) 727 - if err != nil { 728 - return fmt.Errorf("open audit writer: %w", err) 729 - } 730 - defer writer.Close() 731 - if err := writer.AppendJSON(event); err != nil { 732 - return fmt.Errorf("append audit event: %w", err) 733 - } 734 - return nil 735 - } 736 - 737 - func (s *FileStore) readAuditEventsJSONL(path string) ([]AuditEvent, bool, error) { 738 - file, err := os.Open(path) 739 - if err != nil { 740 - if os.IsNotExist(err) { 741 - return nil, false, nil 742 - } 743 - return nil, false, fmt.Errorf("open audit jsonl %s: %w", path, err) 744 - } 745 - defer file.Close() 746 - 747 - records := make([]AuditEvent, 0, 64) 748 - scanner := bufio.NewScanner(file) 749 - scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) 750 - for scanner.Scan() { 751 - line := bytes.TrimSpace(scanner.Bytes()) 752 - if len(line) == 0 { 753 - continue 754 - } 755 - var event AuditEvent 756 - if err := json.Unmarshal(line, &event); err != nil { 757 - return nil, false, fmt.Errorf("decode audit jsonl %s: %w", path, err) 758 - } 759 - records = append(records, event) 760 - } 761 - if err := scanner.Err(); err != nil { 762 - return nil, false, fmt.Errorf("scan audit jsonl %s: %w", path, err) 763 - } 764 - return records, true, nil 765 - } 766 - 767 - func (s *FileStore) readInboxMessagesJSONL(path string) ([]InboxMessage, bool, error) { 768 - file, err := os.Open(path) 769 - if err != nil { 770 - if os.IsNotExist(err) { 771 - return nil, false, nil 772 - } 773 - return nil, false, fmt.Errorf("open inbox jsonl %s: %w", path, err) 774 - } 775 - defer file.Close() 776 - 777 - records := make([]InboxMessage, 0, 64) 778 - scanner := bufio.NewScanner(file) 779 - scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) 780 - for scanner.Scan() { 781 - line := bytes.TrimSpace(scanner.Bytes()) 782 - if len(line) == 0 { 783 - continue 784 - } 785 - var message InboxMessage 786 - if err := json.Unmarshal(line, &message); err != nil { 787 - return nil, false, fmt.Errorf("decode inbox jsonl %s: %w", path, err) 788 - } 789 - message.SessionID = strings.TrimSpace(message.SessionID) 790 - message.ReplyTo = strings.TrimSpace(message.ReplyTo) 791 - records = append(records, message) 792 - } 793 - if err := scanner.Err(); err != nil { 794 - return nil, false, fmt.Errorf("scan inbox jsonl %s: %w", path, err) 795 - } 796 - return records, true, nil 797 - } 798 - 799 - func (s *FileStore) readOutboxMessagesJSONL(path string) ([]OutboxMessage, bool, error) { 800 - file, err := os.Open(path) 801 - if err != nil { 802 - if os.IsNotExist(err) { 803 - return nil, false, nil 804 - } 805 - return nil, false, fmt.Errorf("open outbox jsonl %s: %w", path, err) 806 - } 807 - defer file.Close() 808 - 809 - records := make([]OutboxMessage, 0, 64) 810 - scanner := bufio.NewScanner(file) 811 - scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) 812 - for scanner.Scan() { 813 - line := bytes.TrimSpace(scanner.Bytes()) 814 - if len(line) == 0 { 815 - continue 816 - } 817 - var message OutboxMessage 818 - if err := json.Unmarshal(line, &message); err != nil { 819 - return nil, false, fmt.Errorf("decode outbox jsonl %s: %w", path, err) 820 - } 821 - message.SessionID = strings.TrimSpace(message.SessionID) 822 - message.ReplyTo = strings.TrimSpace(message.ReplyTo) 823 - records = append(records, message) 824 - } 825 - if err := scanner.Err(); err != nil { 826 - return nil, false, fmt.Errorf("scan outbox jsonl %s: %w", path, err) 827 - } 828 - return records, true, nil 829 - } 830 - 831 - func (s *FileStore) ensureNotCanceled(ctx context.Context) error { 832 - if ctx == nil { 833 - return nil 834 - } 835 - select { 836 - case <-ctx.Done(): 837 - return ctx.Err() 838 - default: 839 - return nil 840 - } 841 - } 842 - 843 - func (s *FileStore) rootPath() string { 844 - root := strings.TrimSpace(s.root) 845 - if root == "" { 846 - return "maep" 847 - } 848 - return filepath.Clean(root) 849 - } 850 - 851 - func (s *FileStore) lockRootPath() string { 852 - return filepath.Join(s.rootPath(), ".fslocks") 853 - } 854 - 855 - func (s *FileStore) withStateLock(ctx context.Context, fn func() error) error { 856 - return s.withLock(ctx, "state.main", fn) 857 - } 858 - 859 - func (s *FileStore) withAuditLock(ctx context.Context, fn func() error) error { 860 - return s.withLock(ctx, "audit.audit_events_jsonl", fn) 861 - } 862 - 863 - func (s *FileStore) withLock(ctx context.Context, key string, fn func() error) error { 864 - lockPath, err := fsstore.BuildLockPath(s.lockRootPath(), key) 865 - if err != nil { 866 - return err 867 - } 868 - return fsstore.WithLock(ctx, lockPath, fn) 869 - } 870 - 871 - func (s *FileStore) identityPath() string { 872 - return filepath.Join(s.rootPath(), "identity.json") 873 - } 874 - 875 - func (s *FileStore) contactsPath() string { 876 - return filepath.Join(s.rootPath(), "contacts.json") 877 - } 878 - 879 - func (s *FileStore) auditPathJSONL() string { 880 - return filepath.Join(s.rootPath(), "audit_events.jsonl") 881 - } 882 - 883 - func (s *FileStore) dedupePath() string { 884 - return filepath.Join(s.rootPath(), "dedupe_records.json") 885 - } 886 - 887 - func (s *FileStore) inboxPathJSONL() string { 888 - return filepath.Join(s.rootPath(), "inbox_messages.jsonl") 889 - } 890 - 891 - func (s *FileStore) outboxPathJSONL() string { 892 - return filepath.Join(s.rootPath(), "outbox_messages.jsonl") 893 - } 894 - 895 - func (s *FileStore) protocolHistoryPath() string { 896 - return filepath.Join(s.rootPath(), "protocol_history.json") 897 - }
-314
maep/file_store_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "path/filepath" 6 - "testing" 7 - "time" 8 - ) 9 - 10 - func TestFileStoreIdentityAndContacts(t *testing.T) { 11 - ctx := context.Background() 12 - root := filepath.Join(t.TempDir(), "maep") 13 - store := NewFileStore(root) 14 - if err := store.Ensure(ctx); err != nil { 15 - t.Fatalf("Ensure() error = %v", err) 16 - } 17 - 18 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 19 - identity := Identity{ 20 - NodeUUID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", 21 - PeerID: "12D3KooWexample", 22 - NodeID: "maep:12D3KooWexample", 23 - IdentityPubEd25519: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 24 - IdentityPrivEd25519: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 25 - CreatedAt: now, 26 - UpdatedAt: now, 27 - } 28 - if err := store.PutIdentity(ctx, identity); err != nil { 29 - t.Fatalf("PutIdentity() error = %v", err) 30 - } 31 - 32 - gotIdentity, ok, err := store.GetIdentity(ctx) 33 - if err != nil { 34 - t.Fatalf("GetIdentity() error = %v", err) 35 - } 36 - if !ok { 37 - t.Fatalf("GetIdentity() expected ok=true") 38 - } 39 - if gotIdentity.PeerID != identity.PeerID { 40 - t.Fatalf("identity peer_id mismatch: got %s want %s", gotIdentity.PeerID, identity.PeerID) 41 - } 42 - 43 - contact := Contact{ 44 - NodeUUID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f789", 45 - PeerID: "12D3KooWcontact", 46 - NodeID: "maep:12D3KooWcontact", 47 - IdentityPubEd25519: "ccccccccccccccccccccccccccccccccccccccccccc", 48 - Addresses: []string{"/dns4/example.com/udp/4001/quic-v1/p2p/12D3KooWcontact"}, 49 - MinSupportedProtocol: 1, 50 - MaxSupportedProtocol: 1, 51 - IssuedAt: now, 52 - CardSigAlg: ContactCardSigAlgEd25519, 53 - CardSigFormat: ContactCardSigFormatJCS, 54 - CardSig: "sig", 55 - TrustState: TrustStateTOFU, 56 - CreatedAt: now, 57 - UpdatedAt: now, 58 - } 59 - if err := store.PutContact(ctx, contact); err != nil { 60 - t.Fatalf("PutContact() error = %v", err) 61 - } 62 - 63 - gotContact, ok, err := store.GetContactByPeerID(ctx, contact.PeerID) 64 - if err != nil { 65 - t.Fatalf("GetContactByPeerID() error = %v", err) 66 - } 67 - if !ok { 68 - t.Fatalf("GetContactByPeerID() expected ok=true") 69 - } 70 - if gotContact.NodeUUID != contact.NodeUUID { 71 - t.Fatalf("contact node_uuid mismatch: got %s want %s", gotContact.NodeUUID, contact.NodeUUID) 72 - } 73 - } 74 - 75 - func TestFileStoreDedupeAndProtocolHistory(t *testing.T) { 76 - ctx := context.Background() 77 - root := filepath.Join(t.TempDir(), "maep") 78 - store := NewFileStore(root) 79 - if err := store.Ensure(ctx); err != nil { 80 - t.Fatalf("Ensure() error = %v", err) 81 - } 82 - 83 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 84 - record := DedupeRecord{ 85 - FromPeerID: "12D3KooWpeerA", 86 - Topic: "chat.message", 87 - IdempotencyKey: "m-001", 88 - CreatedAt: now, 89 - // Keep expiration relative to wall clock because GetDedupeRecord uses time.Now(). 90 - ExpiresAt: time.Now().UTC().Add(24 * time.Hour), 91 - } 92 - if err := store.PutDedupeRecord(ctx, record); err != nil { 93 - t.Fatalf("PutDedupeRecord() error = %v", err) 94 - } 95 - gotRecord, ok, err := store.GetDedupeRecord(ctx, record.FromPeerID, record.Topic, record.IdempotencyKey) 96 - if err != nil { 97 - t.Fatalf("GetDedupeRecord() error = %v", err) 98 - } 99 - if !ok { 100 - t.Fatalf("GetDedupeRecord() expected ok=true") 101 - } 102 - if gotRecord.IdempotencyKey != record.IdempotencyKey { 103 - t.Fatalf("dedupe idempotency_key mismatch: got %s want %s", gotRecord.IdempotencyKey, record.IdempotencyKey) 104 - } 105 - 106 - history := ProtocolHistory{ 107 - PeerID: "12D3KooWpeerB", 108 - LastRemoteMaxProtocol: 1, 109 - LastNegotiatedProtocol: 1, 110 - UpdatedAt: now, 111 - } 112 - if err := store.PutProtocolHistory(ctx, history); err != nil { 113 - t.Fatalf("PutProtocolHistory() error = %v", err) 114 - } 115 - gotHistory, ok, err := store.GetProtocolHistory(ctx, history.PeerID) 116 - if err != nil { 117 - t.Fatalf("GetProtocolHistory() error = %v", err) 118 - } 119 - if !ok { 120 - t.Fatalf("GetProtocolHistory() expected ok=true") 121 - } 122 - if gotHistory.LastNegotiatedProtocol != history.LastNegotiatedProtocol { 123 - t.Fatalf("protocol history negotiated mismatch: got %d want %d", gotHistory.LastNegotiatedProtocol, history.LastNegotiatedProtocol) 124 - } 125 - 126 - messageA := InboxMessage{ 127 - MessageID: "msg-001", 128 - FromPeerID: "12D3KooWpeerA", 129 - Topic: "chat.message", 130 - ContentType: "application/json", 131 - PayloadBase64: "eyJ0ZXh0IjoiaGV5In0", 132 - IdempotencyKey: "m-001", 133 - SessionID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", 134 - ReceivedAt: now, 135 - } 136 - if err := store.AppendInboxMessage(ctx, messageA); err != nil { 137 - t.Fatalf("AppendInboxMessage() error = %v", err) 138 - } 139 - messageB := InboxMessage{ 140 - MessageID: "msg-002", 141 - FromPeerID: "12D3KooWpeerB", 142 - Topic: "chat.message", 143 - ContentType: "text/plain", 144 - PayloadBase64: "aGVsbG8", 145 - IdempotencyKey: "m-101", 146 - SessionID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f457", 147 - ReceivedAt: now.Add(5 * time.Second), 148 - } 149 - if err := store.AppendInboxMessage(ctx, messageB); err != nil { 150 - t.Fatalf("AppendInboxMessage() second error = %v", err) 151 - } 152 - 153 - inbox, err := store.ListInboxMessages(ctx, "12D3KooWpeerA", "", 10) 154 - if err != nil { 155 - t.Fatalf("ListInboxMessages() error = %v", err) 156 - } 157 - if len(inbox) != 1 { 158 - t.Fatalf("ListInboxMessages() length mismatch: got %d want 1", len(inbox)) 159 - } 160 - if inbox[0].MessageID != messageA.MessageID { 161 - t.Fatalf("inbox message_id mismatch: got %s want %s", inbox[0].MessageID, messageA.MessageID) 162 - } 163 - if inbox[0].SessionID != messageA.SessionID { 164 - t.Fatalf("inbox session_id mismatch: got %s want %s", inbox[0].SessionID, messageA.SessionID) 165 - } 166 - 167 - outboxA := OutboxMessage{ 168 - MessageID: "msg-901", 169 - ToPeerID: "12D3KooWpeerB", 170 - Topic: "dm.reply.v1", 171 - ContentType: "application/json", 172 - PayloadBase64: "eyJ0ZXh0IjoicG9uZyJ9", 173 - IdempotencyKey: "r-001", 174 - SessionID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f458", 175 - SentAt: now.Add(15 * time.Second), 176 - } 177 - if err := store.AppendOutboxMessage(ctx, outboxA); err != nil { 178 - t.Fatalf("AppendOutboxMessage() error = %v", err) 179 - } 180 - outbox, err := store.ListOutboxMessages(ctx, "12D3KooWpeerB", "", 10) 181 - if err != nil { 182 - t.Fatalf("ListOutboxMessages() error = %v", err) 183 - } 184 - if len(outbox) != 1 { 185 - t.Fatalf("ListOutboxMessages() length mismatch: got %d want 1", len(outbox)) 186 - } 187 - if outbox[0].MessageID != outboxA.MessageID { 188 - t.Fatalf("outbox message_id mismatch: got %s want %s", outbox[0].MessageID, outboxA.MessageID) 189 - } 190 - 191 - audit := AuditEvent{ 192 - EventID: "evt-001", 193 - Action: AuditActionTrustStateChanged, 194 - PeerID: "12D3KooWpeerA", 195 - NodeUUID: "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f789", 196 - PreviousTrustState: TrustStateTOFU, 197 - NewTrustState: TrustStateVerified, 198 - Reason: "manual_verify", 199 - CreatedAt: now.Add(10 * time.Second), 200 - } 201 - if err := store.AppendAuditEvent(ctx, audit); err != nil { 202 - t.Fatalf("AppendAuditEvent() error = %v", err) 203 - } 204 - audits, err := store.ListAuditEvents(ctx, "12D3KooWpeerA", AuditActionTrustStateChanged, 10) 205 - if err != nil { 206 - t.Fatalf("ListAuditEvents() error = %v", err) 207 - } 208 - if len(audits) != 1 { 209 - t.Fatalf("ListAuditEvents() length mismatch: got %d want 1", len(audits)) 210 - } 211 - if audits[0].EventID != audit.EventID { 212 - t.Fatalf("audit event_id mismatch: got %s want %s", audits[0].EventID, audit.EventID) 213 - } 214 - } 215 - 216 - func TestFileStorePruneDedupeRecords_GlobalMaxEntriesAndTTL(t *testing.T) { 217 - ctx := context.Background() 218 - root := filepath.Join(t.TempDir(), "maep") 219 - store := NewFileStore(root) 220 - if err := store.Ensure(ctx); err != nil { 221 - t.Fatalf("Ensure() error = %v", err) 222 - } 223 - 224 - now := time.Date(2026, 2, 7, 4, 0, 0, 0, time.UTC) 225 - for i := 0; i < 5; i++ { 226 - record := DedupeRecord{ 227 - FromPeerID: "12D3KooWpeerA", 228 - Topic: "chat.message", 229 - IdempotencyKey: "m-" + time.Date(2026, 2, 7, 4, 0, i, 0, time.UTC).Format("150405"), 230 - CreatedAt: now.Add(time.Duration(i) * time.Minute), 231 - ExpiresAt: now.Add(24 * time.Hour), 232 - } 233 - if err := store.PutDedupeRecord(ctx, record); err != nil { 234 - t.Fatalf("PutDedupeRecord() error = %v", err) 235 - } 236 - } 237 - expired := DedupeRecord{ 238 - FromPeerID: "12D3KooWpeerA", 239 - Topic: "chat.message", 240 - IdempotencyKey: "m-expired", 241 - CreatedAt: now.Add(-48 * time.Hour), 242 - ExpiresAt: now.Add(-24 * time.Hour), 243 - } 244 - if err := store.PutDedupeRecord(ctx, expired); err != nil { 245 - t.Fatalf("PutDedupeRecord(expired) error = %v", err) 246 - } 247 - 248 - removed, err := store.PruneDedupeRecords(ctx, now, 3) 249 - if err != nil { 250 - t.Fatalf("PruneDedupeRecords() error = %v", err) 251 - } 252 - if removed != 3 { 253 - t.Fatalf("PruneDedupeRecords() removed mismatch: got %d want 3", removed) 254 - } 255 - 256 - records, err := store.loadDedupeRecordsLocked() 257 - if err != nil { 258 - t.Fatalf("loadDedupeRecordsLocked() error = %v", err) 259 - } 260 - if len(records) != 3 { 261 - t.Fatalf("remaining dedupe records mismatch: got %d want 3", len(records)) 262 - } 263 - } 264 - 265 - func TestSessionScopeByTopic_DialogueTopicsShareScope(t *testing.T) { 266 - share := SessionScopeByTopic("share.proactive.v1") 267 - reply := SessionScopeByTopic("dm.reply.v1") 268 - checkin := SessionScopeByTopic("dm.checkin.v1") 269 - chat := SessionScopeByTopic("chat.message") 270 - 271 - want := "dialogue.v1" 272 - for _, got := range []string{share, reply, checkin, chat} { 273 - if got != want { 274 - t.Fatalf("SessionScopeByTopic mismatch: got %q want %q", got, want) 275 - } 276 - } 277 - 278 - other := SessionScopeByTopic("agent.status.v1") 279 - if other != "topic:agent.status.v1" { 280 - t.Fatalf("SessionScopeByTopic for non-dialogue topic mismatch: got %q", other) 281 - } 282 - } 283 - 284 - func TestFileStoreAppendInboxMessage_DoesNotAutoFillSessionID(t *testing.T) { 285 - ctx := context.Background() 286 - root := filepath.Join(t.TempDir(), "maep") 287 - store := NewFileStore(root) 288 - if err := store.Ensure(ctx); err != nil { 289 - t.Fatalf("Ensure() error = %v", err) 290 - } 291 - 292 - msg := InboxMessage{ 293 - MessageID: "msg-raw-1", 294 - FromPeerID: "12D3KooWpeerZ", 295 - Topic: "agent.status.v1", 296 - ContentType: "text/plain", 297 - PayloadBase64: "aGVsbG8", 298 - IdempotencyKey: "id-1", 299 - } 300 - if err := store.AppendInboxMessage(ctx, msg); err != nil { 301 - t.Fatalf("AppendInboxMessage() error = %v", err) 302 - } 303 - 304 - records, err := store.ListInboxMessages(ctx, msg.FromPeerID, msg.Topic, 10) 305 - if err != nil { 306 - t.Fatalf("ListInboxMessages() error = %v", err) 307 - } 308 - if len(records) != 1 { 309 - t.Fatalf("ListInboxMessages() len mismatch: got %d want 1", len(records)) 310 - } 311 - if records[0].SessionID != "" { 312 - t.Fatalf("expected empty session_id, got %q", records[0].SessionID) 313 - } 314 - }
-135
maep/identity.go
··· 1 - package maep 2 - 3 - import ( 4 - "crypto/rand" 5 - "crypto/sha256" 6 - "encoding/base64" 7 - "encoding/hex" 8 - "fmt" 9 - "strings" 10 - "time" 11 - 12 - "github.com/google/uuid" 13 - ic "github.com/libp2p/go-libp2p/core/crypto" 14 - "github.com/libp2p/go-libp2p/core/peer" 15 - ) 16 - 17 - func GenerateIdentity(now time.Time) (Identity, error) { 18 - if now.IsZero() { 19 - now = time.Now().UTC() 20 - } 21 - now = now.UTC() 22 - 23 - nodeUUID, err := uuid.NewV7() 24 - if err != nil { 25 - return Identity{}, fmt.Errorf("generate uuid v7: %w", err) 26 - } 27 - 28 - priv, pub, err := ic.GenerateEd25519Key(rand.Reader) 29 - if err != nil { 30 - return Identity{}, fmt.Errorf("generate ed25519 keypair: %w", err) 31 - } 32 - 33 - peerID, err := peer.IDFromPublicKey(pub) 34 - if err != nil { 35 - return Identity{}, fmt.Errorf("derive peer id from public key: %w", err) 36 - } 37 - 38 - pubRaw, err := pub.Raw() 39 - if err != nil { 40 - return Identity{}, fmt.Errorf("export public key bytes: %w", err) 41 - } 42 - if len(pubRaw) != ed25519PublicKeyBytes { 43 - return Identity{}, fmt.Errorf("invalid ed25519 public key bytes length: %d", len(pubRaw)) 44 - } 45 - 46 - privRaw, err := priv.Raw() 47 - if err != nil { 48 - return Identity{}, fmt.Errorf("export private key bytes: %w", err) 49 - } 50 - 51 - return Identity{ 52 - NodeUUID: nodeUUID.String(), 53 - PeerID: peerID.String(), 54 - NodeID: NodeIDFromPeerID(peerID.String()), 55 - IdentityPubEd25519: encodeBase64URL(pubRaw), 56 - IdentityPrivEd25519: encodeBase64URL(privRaw), 57 - CreatedAt: now, 58 - UpdatedAt: now, 59 - }, nil 60 - } 61 - 62 - func ParseIdentityPublicKey(identityPubEd25519 string) ([]byte, error) { 63 - pubRaw, err := decodeBase64URL(identityPubEd25519) 64 - if err != nil { 65 - return nil, WrapProtocolError(ErrInvalidContactCard, "identity_pub_ed25519 decode failed: %v", err) 66 - } 67 - if len(pubRaw) != ed25519PublicKeyBytes { 68 - return nil, WrapProtocolError(ErrInvalidContactCard, "identity_pub_ed25519 must decode to %d bytes, got %d", ed25519PublicKeyBytes, len(pubRaw)) 69 - } 70 - return pubRaw, nil 71 - } 72 - 73 - func ParseIdentityPrivateKey(identityPrivEd25519 string) (ic.PrivKey, error) { 74 - privRaw, err := decodeBase64URL(identityPrivEd25519) 75 - if err != nil { 76 - return nil, fmt.Errorf("identity_priv_ed25519 decode failed: %w", err) 77 - } 78 - priv, err := ic.UnmarshalEd25519PrivateKey(privRaw) 79 - if err != nil { 80 - return nil, fmt.Errorf("unmarshal ed25519 private key: %w", err) 81 - } 82 - return priv, nil 83 - } 84 - 85 - func DerivePeerIDFromIdentityPub(identityPubEd25519 string) (peer.ID, error) { 86 - pubRaw, err := ParseIdentityPublicKey(identityPubEd25519) 87 - if err != nil { 88 - return "", err 89 - } 90 - pub, err := ic.UnmarshalEd25519PublicKey(pubRaw) 91 - if err != nil { 92 - return "", WrapProtocolError(ErrInvalidContactCard, "unmarshal ed25519 public key failed: %v", err) 93 - } 94 - derivedPeerID, err := peer.IDFromPublicKey(pub) 95 - if err != nil { 96 - return "", WrapProtocolError(ErrInvalidContactCard, "derive peer id failed: %v", err) 97 - } 98 - return derivedPeerID, nil 99 - } 100 - 101 - func FingerprintHex(identityPubEd25519 string) (string, error) { 102 - pubRaw, err := ParseIdentityPublicKey(identityPubEd25519) 103 - if err != nil { 104 - return "", err 105 - } 106 - sum := sha256.Sum256(pubRaw) 107 - return hex.EncodeToString(sum[:]), nil 108 - } 109 - 110 - func FingerprintGrouped(identityPubEd25519 string) (string, error) { 111 - hexFingerprint, err := FingerprintHex(identityPubEd25519) 112 - if err != nil { 113 - return "", err 114 - } 115 - if len(hexFingerprint) != 64 { 116 - return hexFingerprint, nil 117 - } 118 - parts := make([]string, 0, 8) 119 - for i := 0; i < len(hexFingerprint); i += 8 { 120 - parts = append(parts, hexFingerprint[i:i+8]) 121 - } 122 - return strings.Join(parts, "-"), nil 123 - } 124 - 125 - func NodeIDFromPeerID(peerID string) string { 126 - return NodeIDPrefix + strings.TrimSpace(peerID) 127 - } 128 - 129 - func encodeBase64URL(raw []byte) string { 130 - return base64.RawURLEncoding.EncodeToString(raw) 131 - } 132 - 133 - func decodeBase64URL(encoded string) ([]byte, error) { 134 - return base64.RawURLEncoding.DecodeString(strings.TrimSpace(encoded)) 135 - }
-153
maep/jsonprofile.go
··· 1 - package maep 2 - 3 - import ( 4 - "bytes" 5 - "encoding/json" 6 - "io" 7 - "regexp" 8 - "strings" 9 - ) 10 - 11 - var jsonIntegerPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`) 12 - 13 - func ValidateStrictJSONProfile(data []byte) error { 14 - if len(bytes.TrimSpace(data)) == 0 { 15 - return WrapProtocolError(ErrInvalidJSONProfile, "empty JSON") 16 - } 17 - 18 - dec := json.NewDecoder(bytes.NewReader(data)) 19 - dec.UseNumber() 20 - if err := validateJSONValue(dec); err != nil { 21 - return err 22 - } 23 - 24 - _, err := dec.Token() 25 - if err == io.EOF { 26 - return nil 27 - } 28 - if err != nil { 29 - return WrapProtocolError(ErrInvalidJSONProfile, "invalid trailing token: %v", err) 30 - } 31 - return WrapProtocolError(ErrInvalidJSONProfile, "trailing JSON value is not allowed") 32 - } 33 - 34 - func validateJSONValue(dec *json.Decoder) error { 35 - tok, err := dec.Token() 36 - if err != nil { 37 - if err == io.EOF { 38 - return WrapProtocolError(ErrInvalidJSONProfile, "unexpected end of input") 39 - } 40 - return WrapProtocolError(ErrInvalidJSONProfile, "token decode failed: %v", err) 41 - } 42 - return validateTokenValue(dec, tok) 43 - } 44 - 45 - func validateTokenValue(dec *json.Decoder, tok json.Token) error { 46 - switch t := tok.(type) { 47 - case json.Delim: 48 - switch t { 49 - case '{': 50 - return validateJSONObject(dec) 51 - case '[': 52 - return validateJSONArray(dec) 53 - default: 54 - return WrapProtocolError(ErrInvalidJSONProfile, "unexpected delimiter %q", string(t)) 55 - } 56 - case nil: 57 - return WrapProtocolError(ErrInvalidJSONProfile, "null is not allowed") 58 - case json.Number: 59 - if !isStrictInteger(t.String()) { 60 - return WrapProtocolError(ErrInvalidJSONProfile, "float number is not allowed: %s", t.String()) 61 - } 62 - return nil 63 - case string, bool: 64 - return nil 65 - default: 66 - return WrapProtocolError(ErrInvalidJSONProfile, "unsupported token type %T", tok) 67 - } 68 - } 69 - 70 - func validateJSONObject(dec *json.Decoder) error { 71 - seen := map[string]struct{}{} 72 - for dec.More() { 73 - keyTok, err := dec.Token() 74 - if err != nil { 75 - return WrapProtocolError(ErrInvalidJSONProfile, "failed to read object key: %v", err) 76 - } 77 - key, ok := keyTok.(string) 78 - if !ok { 79 - return WrapProtocolError(ErrInvalidJSONProfile, "object key must be string") 80 - } 81 - if _, exists := seen[key]; exists { 82 - return WrapProtocolError(ErrInvalidJSONProfile, "duplicate object key: %q", key) 83 - } 84 - seen[key] = struct{}{} 85 - 86 - if err := validateJSONValue(dec); err != nil { 87 - return err 88 - } 89 - } 90 - endTok, err := dec.Token() 91 - if err != nil { 92 - return WrapProtocolError(ErrInvalidJSONProfile, "object not closed: %v", err) 93 - } 94 - delim, ok := endTok.(json.Delim) 95 - if !ok || delim != '}' { 96 - return WrapProtocolError(ErrInvalidJSONProfile, "object close delimiter is invalid") 97 - } 98 - return nil 99 - } 100 - 101 - func validateJSONArray(dec *json.Decoder) error { 102 - for dec.More() { 103 - if err := validateJSONValue(dec); err != nil { 104 - return err 105 - } 106 - } 107 - endTok, err := dec.Token() 108 - if err != nil { 109 - return WrapProtocolError(ErrInvalidJSONProfile, "array not closed: %v", err) 110 - } 111 - delim, ok := endTok.(json.Delim) 112 - if !ok || delim != ']' { 113 - return WrapProtocolError(ErrInvalidJSONProfile, "array close delimiter is invalid") 114 - } 115 - return nil 116 - } 117 - 118 - func isStrictInteger(raw string) bool { 119 - raw = strings.TrimSpace(raw) 120 - if raw == "" { 121 - return false 122 - } 123 - if strings.ContainsAny(raw, ".eE") { 124 - return false 125 - } 126 - return jsonIntegerPattern.MatchString(raw) 127 - } 128 - 129 - func decodeStrictJSON(data []byte, out any) error { 130 - if err := ValidateStrictJSONProfile(data); err != nil { 131 - return err 132 - } 133 - dec := json.NewDecoder(bytes.NewReader(data)) 134 - dec.UseNumber() 135 - if err := dec.Decode(out); err != nil { 136 - return WrapProtocolError(ErrInvalidJSONProfile, "JSON decode failed: %v", err) 137 - } 138 - if dec.More() { 139 - return WrapProtocolError(ErrInvalidJSONProfile, "trailing JSON input is not allowed") 140 - } 141 - return nil 142 - } 143 - 144 - func mustJSONObject(raw []byte) error { 145 - var probe any 146 - if err := decodeStrictJSON(raw, &probe); err != nil { 147 - return err 148 - } 149 - if _, ok := probe.(map[string]any); !ok { 150 - return WrapProtocolError(ErrInvalidJSONProfile, "top-level JSON value must be an object") 151 - } 152 - return nil 153 - }
-28
maep/jsonprofile_test.go
··· 1 - package maep 2 - 3 - import "testing" 4 - 5 - func TestValidateStrictJSONProfile(t *testing.T) { 6 - tests := []struct { 7 - name string 8 - raw string 9 - wantErr bool 10 - }{ 11 - {name: "valid", raw: `{"a":1,"b":[2,3],"c":{"d":"x"}}`, wantErr: false}, 12 - {name: "float", raw: `{"a":1.25}`, wantErr: true}, 13 - {name: "null", raw: `{"a":null}`, wantErr: true}, 14 - {name: "duplicate key", raw: `{"a":1,"a":2}`, wantErr: true}, 15 - } 16 - 17 - for _, tc := range tests { 18 - t.Run(tc.name, func(t *testing.T) { 19 - err := ValidateStrictJSONProfile([]byte(tc.raw)) 20 - if tc.wantErr && err == nil { 21 - t.Fatalf("expected error, got nil") 22 - } 23 - if !tc.wantErr && err != nil { 24 - t.Fatalf("unexpected error: %v", err) 25 - } 26 - }) 27 - } 28 - }
-1096
maep/node.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "encoding/base64" 6 - "encoding/json" 7 - "fmt" 8 - "io" 9 - "log/slog" 10 - "strings" 11 - "sync" 12 - "time" 13 - 14 - "github.com/google/uuid" 15 - libp2p "github.com/libp2p/go-libp2p" 16 - "github.com/libp2p/go-libp2p/core/host" 17 - "github.com/libp2p/go-libp2p/core/network" 18 - "github.com/libp2p/go-libp2p/core/peer" 19 - "github.com/libp2p/go-libp2p/core/protocol" 20 - ma "github.com/multiformats/go-multiaddr" 21 - ) 22 - 23 - const sessionFreshWindow = 10 * time.Minute 24 - 25 - type NodeOptions struct { 26 - DialOnly bool 27 - ListenAddrs []string 28 - DialAddrTimeout time.Duration 29 - HelloTimeout time.Duration 30 - RPCTimeout time.Duration 31 - MaxRPCRequestBytes int 32 - MaxPayloadBytes int 33 - DataPushPerMinute int 34 - DedupeTTL time.Duration 35 - DedupeMaxEntries int 36 - Logger *slog.Logger 37 - OnDataPush func(event DataPushEvent) 38 - } 39 - 40 - type HelloResult struct { 41 - RemotePeerID string 42 - RemoteMinProtocol int 43 - RemoteMaxProtocol int 44 - NegotiatedProtocol int 45 - UpdatedAt time.Time 46 - } 47 - 48 - type Node struct { 49 - host host.Host 50 - svc *Service 51 - store Store 52 - local Identity 53 - opts NodeOptions 54 - 55 - mu sync.RWMutex 56 - sessions map[string]HelloResult 57 - 58 - rateMu sync.Mutex 59 - pushRateWindows map[string]pushRateWindow 60 - } 61 - 62 - type pushRateWindow struct { 63 - WindowMinute time.Time 64 - Count int 65 - } 66 - 67 - type helloMessage struct { 68 - Type string `json:"type"` 69 - ProtocolMin int `json:"protocol_min"` 70 - ProtocolMax int `json:"protocol_max"` 71 - Capabilities []string `json:"capabilities"` 72 - } 73 - 74 - func NewNode(ctx context.Context, svc *Service, opts NodeOptions) (*Node, error) { 75 - if svc == nil || svc.store == nil { 76 - return nil, fmt.Errorf("nil maep service") 77 - } 78 - identity, ok, err := svc.GetIdentity(ctx) 79 - if err != nil { 80 - return nil, err 81 - } 82 - if !ok { 83 - return nil, fmt.Errorf("identity not found; run `mistermorph maep init`") 84 - } 85 - priv, err := ParseIdentityPrivateKey(identity.IdentityPrivEd25519) 86 - if err != nil { 87 - return nil, err 88 - } 89 - 90 - options := normalizeNodeOptions(opts) 91 - 92 - hostOpts := []libp2p.Option{libp2p.Identity(priv)} 93 - if options.DialOnly { 94 - hostOpts = append(hostOpts, libp2p.NoListenAddrs) 95 - } else { 96 - hostOpts = append(hostOpts, libp2p.ListenAddrStrings(options.ListenAddrs...)) 97 - } 98 - 99 - h, err := libp2p.New(hostOpts...) 100 - if err != nil { 101 - return nil, fmt.Errorf("create libp2p host: %w", err) 102 - } 103 - 104 - if h.ID().String() != identity.PeerID { 105 - _ = h.Close() 106 - return nil, fmt.Errorf("libp2p host identity mismatch: host=%s identity=%s", h.ID().String(), identity.PeerID) 107 - } 108 - 109 - n := &Node{ 110 - host: h, 111 - svc: svc, 112 - store: svc.store, 113 - local: identity, 114 - opts: options, 115 - sessions: map[string]HelloResult{}, 116 - pushRateWindows: map[string]pushRateWindow{}, 117 - } 118 - 119 - h.SetStreamHandler(protocol.ID(ProtocolHelloIDV1), n.handleHelloStream) 120 - h.SetStreamHandler(protocol.ID(ProtocolRPCIDV1), n.handleRPCStream) 121 - 122 - return n, nil 123 - } 124 - 125 - func (n *Node) Close() error { 126 - if n == nil || n.host == nil { 127 - return nil 128 - } 129 - return n.host.Close() 130 - } 131 - 132 - func (n *Node) PeerID() string { 133 - if n == nil || n.host == nil { 134 - return "" 135 - } 136 - return n.host.ID().String() 137 - } 138 - 139 - func (n *Node) AddrStrings() []string { 140 - if n == nil || n.host == nil { 141 - return nil 142 - } 143 - baseAddrs := n.host.Addrs() 144 - out := make([]string, 0, len(baseAddrs)) 145 - for _, addr := range baseAddrs { 146 - p2pComponent, err := ma.NewMultiaddr("/p2p/" + n.host.ID().String()) 147 - if err != nil { 148 - continue 149 - } 150 - out = append(out, addr.Encapsulate(p2pComponent).String()) 151 - } 152 - sortStrings(out) 153 - return out 154 - } 155 - 156 - func (n *Node) Ping(ctx context.Context, peerID string, addresses []string) (map[string]any, error) { 157 - resultRaw, err := n.callRPC(ctx, peerID, addresses, "agent.ping", map[string]any{}, false) 158 - if err != nil { 159 - return nil, err 160 - } 161 - var out map[string]any 162 - if err := decodeStrictJSON(resultRaw, &out); err != nil { 163 - return nil, err 164 - } 165 - return out, nil 166 - } 167 - 168 - func (n *Node) GetCapabilities(ctx context.Context, peerID string, addresses []string) (map[string]any, error) { 169 - resultRaw, err := n.callRPC(ctx, peerID, addresses, "agent.capabilities.get", map[string]any{}, false) 170 - if err != nil { 171 - return nil, err 172 - } 173 - var out map[string]any 174 - if err := decodeStrictJSON(resultRaw, &out); err != nil { 175 - return nil, err 176 - } 177 - return out, nil 178 - } 179 - 180 - func (n *Node) PushData(ctx context.Context, peerID string, addresses []string, req DataPushRequest, notification bool) (DataPushResult, error) { 181 - req.Topic = strings.TrimSpace(req.Topic) 182 - req.ContentType = strings.TrimSpace(req.ContentType) 183 - req.PayloadBase64 = strings.TrimSpace(req.PayloadBase64) 184 - req.IdempotencyKey = strings.TrimSpace(req.IdempotencyKey) 185 - 186 - payloadBytes, decodeErr := base64.RawURLEncoding.DecodeString(req.PayloadBase64) 187 - if decodeErr != nil { 188 - return DataPushResult{}, WrapProtocolError(ErrInvalidParams, "payload_base64 decode failed") 189 - } 190 - sessionID, replyTo, err := extractAndValidateSessionForTopic(req.Topic, req.ContentType, payloadBytes) 191 - if err != nil { 192 - return DataPushResult{}, WrapProtocolError(ErrInvalidParams, "%s", err.Error()) 193 - } 194 - 195 - params := map[string]any{ 196 - "topic": req.Topic, 197 - "content_type": req.ContentType, 198 - "payload_base64": req.PayloadBase64, 199 - "idempotency_key": req.IdempotencyKey, 200 - } 201 - resultRaw, err := n.callRPC(ctx, peerID, addresses, "agent.data.push", params, notification) 202 - if err != nil { 203 - return DataPushResult{}, err 204 - } 205 - result := DataPushResult{Accepted: true, Deduped: false} 206 - if !notification { 207 - if err := decodeStrictJSON(resultRaw, &result); err != nil { 208 - return DataPushResult{}, err 209 - } 210 - } 211 - 212 - now := time.Now().UTC() 213 - outboxMessage := OutboxMessage{ 214 - MessageID: "msg_" + uuid.NewString(), 215 - ToPeerID: strings.TrimSpace(peerID), 216 - Topic: req.Topic, 217 - ContentType: req.ContentType, 218 - PayloadBase64: req.PayloadBase64, 219 - IdempotencyKey: req.IdempotencyKey, 220 - SessionID: sessionID, 221 - ReplyTo: replyTo, 222 - SentAt: now, 223 - } 224 - if err := n.store.AppendOutboxMessage(context.Background(), outboxMessage); err != nil { 225 - n.opts.Logger.Warn("append outbox message failed", "peer_id", peerID, "err", err) 226 - } 227 - 228 - return result, nil 229 - } 230 - 231 - func (n *Node) DialHello(ctx context.Context, peerID string, addresses []string) (HelloResult, error) { 232 - expectedPeerID, dialAddresses, _, err := n.resolveDialTarget(ctx, peerID, addresses) 233 - if err != nil { 234 - return HelloResult{}, err 235 - } 236 - 237 - timeoutCtx, cancel := withTimeoutIfNeeded(ctx, n.opts.HelloTimeout) 238 - defer cancel() 239 - 240 - if err := n.connect(timeoutCtx, expectedPeerID, dialAddresses); err != nil { 241 - return HelloResult{}, err 242 - } 243 - 244 - stream, err := n.host.NewStream(timeoutCtx, expectedPeerID, protocol.ID(ProtocolHelloIDV1)) 245 - if err != nil { 246 - return HelloResult{}, fmt.Errorf("open hello stream: %w", err) 247 - } 248 - defer stream.Close() 249 - _ = stream.SetDeadline(time.Now().UTC().Add(n.opts.HelloTimeout)) 250 - 251 - if err := verifyRemotePeerOnStream(stream, expectedPeerID); err != nil { 252 - _ = stream.Reset() 253 - return HelloResult{}, err 254 - } 255 - 256 - localHello := n.localHelloMessage() 257 - reqRaw, err := json.Marshal(localHello) 258 - if err != nil { 259 - return HelloResult{}, err 260 - } 261 - if _, err := stream.Write(reqRaw); err != nil { 262 - return HelloResult{}, fmt.Errorf("write hello request: %w", err) 263 - } 264 - if err := stream.CloseWrite(); err != nil { 265 - return HelloResult{}, fmt.Errorf("close hello write: %w", err) 266 - } 267 - 268 - respRaw, tooLarge, err := readAllLimited(stream, n.opts.MaxRPCRequestBytes) 269 - if err != nil { 270 - return HelloResult{}, fmt.Errorf("read hello response: %w", err) 271 - } 272 - if tooLarge { 273 - return HelloResult{}, WrapProtocolError(ErrPayloadTooLarge, "hello response exceeds limit") 274 - } 275 - 276 - remoteHello, err := parseHelloMessage(respRaw) 277 - if err != nil { 278 - return HelloResult{}, err 279 - } 280 - negotiated, err := negotiateProtocol(localHello.ProtocolMin, localHello.ProtocolMax, remoteHello.ProtocolMin, remoteHello.ProtocolMax) 281 - if err != nil { 282 - return HelloResult{}, err 283 - } 284 - 285 - result := HelloResult{ 286 - RemotePeerID: expectedPeerID.String(), 287 - RemoteMinProtocol: remoteHello.ProtocolMin, 288 - RemoteMaxProtocol: remoteHello.ProtocolMax, 289 - NegotiatedProtocol: negotiated, 290 - } 291 - if err := n.recordSession(context.Background(), result); err != nil { 292 - n.opts.Logger.Warn("record hello session failed", "peer_id", expectedPeerID.String(), "err", err) 293 - } 294 - return result, nil 295 - } 296 - 297 - func (n *Node) callRPC(ctx context.Context, peerID string, addresses []string, method string, params any, notification bool) (json.RawMessage, error) { 298 - expectedPeerID, dialAddresses, _, err := n.resolveDialTarget(ctx, peerID, addresses) 299 - if err != nil { 300 - return nil, err 301 - } 302 - return n.callRPCResolved(ctx, expectedPeerID, dialAddresses, method, params, notification, false) 303 - } 304 - 305 - func (n *Node) callRPCResolved(ctx context.Context, expectedPeerID peer.ID, dialAddresses []string, method string, params any, notification bool, retriedUnsupported bool) (json.RawMessage, error) { 306 - 307 - if !n.hasFreshSession(expectedPeerID.String()) { 308 - if _, err := n.DialHello(ctx, expectedPeerID.String(), dialAddresses); err != nil { 309 - return nil, err 310 - } 311 - } 312 - 313 - timeoutCtx, cancel := withTimeoutIfNeeded(ctx, n.opts.RPCTimeout) 314 - defer cancel() 315 - 316 - if err := n.connect(timeoutCtx, expectedPeerID, dialAddresses); err != nil { 317 - return nil, err 318 - } 319 - 320 - stream, err := n.host.NewStream(timeoutCtx, expectedPeerID, protocol.ID(ProtocolRPCIDV1)) 321 - if err != nil { 322 - return nil, fmt.Errorf("open rpc stream: %w", err) 323 - } 324 - defer stream.Close() 325 - _ = stream.SetDeadline(time.Now().UTC().Add(n.opts.RPCTimeout)) 326 - 327 - if err := verifyRemotePeerOnStream(stream, expectedPeerID); err != nil { 328 - _ = stream.Reset() 329 - return nil, err 330 - } 331 - 332 - reqObj := map[string]any{ 333 - "jsonrpc": JSONRPCVersion, 334 - "method": method, 335 - "params": params, 336 - } 337 - var requestID any 338 - if !notification { 339 - requestID = generateRPCRequestID() 340 - reqObj["id"] = requestID 341 - } 342 - reqRaw, err := json.Marshal(reqObj) 343 - if err != nil { 344 - return nil, fmt.Errorf("marshal rpc request: %w", err) 345 - } 346 - if len(reqRaw) > n.opts.MaxRPCRequestBytes { 347 - return nil, WrapProtocolError(ErrPayloadTooLarge, "rpc request exceeds limit") 348 - } 349 - 350 - if _, err := stream.Write(reqRaw); err != nil { 351 - return nil, fmt.Errorf("write rpc request: %w", err) 352 - } 353 - if err := stream.CloseWrite(); err != nil { 354 - return nil, fmt.Errorf("close rpc write: %w", err) 355 - } 356 - if notification { 357 - return nil, nil 358 - } 359 - 360 - respRaw, tooLarge, err := readAllLimited(stream, n.opts.MaxRPCRequestBytes) 361 - if err != nil { 362 - return nil, fmt.Errorf("read rpc response: %w", err) 363 - } 364 - if tooLarge { 365 - return nil, WrapProtocolError(ErrPayloadTooLarge, "rpc response exceeds limit") 366 - } 367 - 368 - result, symbol, details, err := parseRPCResponse(respRaw) 369 - if err != nil { 370 - return nil, err 371 - } 372 - if strings.TrimSpace(symbol) != "" { 373 - // Handles session drift (for example remote node restart) by renegotiating once. 374 - if shouldRetryAfterUnsupported(symbol, retriedUnsupported) { 375 - if _, err := n.DialHello(ctx, expectedPeerID.String(), dialAddresses); err != nil { 376 - return nil, err 377 - } 378 - return n.callRPCResolved(ctx, expectedPeerID, dialAddresses, method, params, notification, true) 379 - } 380 - return nil, protocolErrorFromSymbol(symbol, details) 381 - } 382 - return result, nil 383 - } 384 - 385 - func shouldRetryAfterUnsupported(symbol string, retried bool) bool { 386 - if retried { 387 - return false 388 - } 389 - return strings.EqualFold(strings.TrimSpace(symbol), ErrUnsupportedProtocolSymbol) 390 - } 391 - 392 - func (n *Node) handleHelloStream(stream network.Stream) { 393 - defer stream.Close() 394 - _ = stream.SetDeadline(time.Now().UTC().Add(n.opts.HelloTimeout)) 395 - 396 - remotePeer := stream.Conn().RemotePeer().String() 397 - if _, err := n.ensurePeerAllowed(context.Background(), remotePeer); err != nil { 398 - n.opts.Logger.Warn("reject hello from unauthorized peer", "peer_id", remotePeer, "err", err) 399 - _ = stream.Conn().Close() 400 - return 401 - } 402 - 403 - if err := verifyRemotePeerOnStream(stream, stream.Conn().RemotePeer()); err != nil { 404 - n.opts.Logger.Warn("reject hello due to peer mismatch", "peer_id", remotePeer, "err", err) 405 - _ = stream.Conn().Close() 406 - return 407 - } 408 - 409 - reqRaw, tooLarge, err := readAllLimited(stream, n.opts.MaxRPCRequestBytes) 410 - if err != nil { 411 - n.opts.Logger.Warn("read hello request failed", "peer_id", remotePeer, "err", err) 412 - return 413 - } 414 - if tooLarge { 415 - n.opts.Logger.Warn("hello request too large", "peer_id", remotePeer) 416 - _ = stream.Conn().Close() 417 - return 418 - } 419 - 420 - remoteHello, err := parseHelloMessage(reqRaw) 421 - if err != nil { 422 - n.opts.Logger.Warn("invalid hello request", "peer_id", remotePeer, "err", err) 423 - _ = stream.Conn().Close() 424 - return 425 - } 426 - 427 - localHello := n.localHelloMessage() 428 - negotiated, err := negotiateProtocol(localHello.ProtocolMin, localHello.ProtocolMax, remoteHello.ProtocolMin, remoteHello.ProtocolMax) 429 - if err != nil { 430 - n.opts.Logger.Warn("hello negotiation failed", "peer_id", remotePeer, "err", err) 431 - _ = stream.Conn().Close() 432 - return 433 - } 434 - 435 - result := HelloResult{ 436 - RemotePeerID: remotePeer, 437 - RemoteMinProtocol: remoteHello.ProtocolMin, 438 - RemoteMaxProtocol: remoteHello.ProtocolMax, 439 - NegotiatedProtocol: negotiated, 440 - } 441 - if err := n.recordSession(context.Background(), result); err != nil { 442 - n.opts.Logger.Warn("record hello session failed", "peer_id", remotePeer, "err", err) 443 - } 444 - 445 - respRaw, err := json.Marshal(localHello) 446 - if err != nil { 447 - n.opts.Logger.Warn("marshal hello response failed", "peer_id", remotePeer, "err", err) 448 - return 449 - } 450 - if _, err := stream.Write(respRaw); err != nil { 451 - n.opts.Logger.Warn("write hello response failed", "peer_id", remotePeer, "err", err) 452 - } 453 - } 454 - 455 - func (n *Node) handleRPCStream(stream network.Stream) { 456 - defer stream.Close() 457 - _ = stream.SetDeadline(time.Now().UTC().Add(n.opts.RPCTimeout)) 458 - 459 - remotePeerID := stream.Conn().RemotePeer().String() 460 - if err := verifyRemotePeerOnStream(stream, stream.Conn().RemotePeer()); err != nil { 461 - n.opts.Logger.Warn("reject rpc due to peer mismatch", "peer_id", remotePeerID, "err", err) 462 - _ = stream.Conn().Close() 463 - return 464 - } 465 - 466 - raw, tooLarge, err := readAllLimited(stream, n.opts.MaxRPCRequestBytes) 467 - if err != nil { 468 - n.opts.Logger.Warn("read rpc request failed", "peer_id", remotePeerID, "err", err) 469 - return 470 - } 471 - if tooLarge { 472 - _, _ = n.writeRPCError(stream, nil, ErrPayloadTooLargeSymbol, "request exceeds max_rpc_request_bytes") 473 - return 474 - } 475 - 476 - req, err := parseRPCRequest(raw) 477 - if err != nil { 478 - n.opts.Logger.Warn("invalid rpc request", "peer_id", remotePeerID, "err", err) 479 - if reqID, hasID := extractRPCIDForError(raw); hasID { 480 - symbol := SymbolOf(err) 481 - if strings.TrimSpace(symbol) == "" { 482 - symbol = ErrInvalidParamsSymbol 483 - } 484 - _, _ = n.writeRPCError(stream, reqID, symbol, err.Error()) 485 - } 486 - return 487 - } 488 - 489 - if !n.hasFreshSession(remotePeerID) { 490 - if req.HasID { 491 - _, _ = n.writeRPCError(stream, req.ID, ErrUnsupportedProtocolSymbol, "hello negotiation required before rpc") 492 - } 493 - return 494 - } 495 - 496 - if _, err := n.ensurePeerAllowed(context.Background(), remotePeerID); err != nil { 497 - if req.HasID { 498 - _, _ = n.writeRPCError(stream, req.ID, ErrUnauthorizedSymbol, err.Error()) 499 - } 500 - _ = stream.Conn().Close() 501 - return 502 - } 503 - 504 - if !isAllowedMethod(req.Method) { 505 - if req.HasID { 506 - _, _ = n.writeRPCError(stream, req.ID, ErrMethodNotAllowedSymbol, "method="+req.Method) 507 - } 508 - return 509 - } 510 - 511 - result, symbol, details := n.handleRPCMethod(remotePeerID, req) 512 - if !req.HasID { 513 - if symbol != "" { 514 - n.opts.Logger.Warn("rpc notification rejected", "peer_id", remotePeerID, "method", req.Method, "symbol", symbol, "details", details) 515 - } 516 - return 517 - } 518 - 519 - if strings.TrimSpace(symbol) != "" { 520 - _, _ = n.writeRPCError(stream, req.ID, symbol, details) 521 - return 522 - } 523 - _, _ = n.writeRPCSuccess(stream, req.ID, result) 524 - } 525 - 526 - func (n *Node) handleRPCMethod(fromPeerID string, req rpcRequest) (any, string, string) { 527 - switch req.Method { 528 - case "agent.ping": 529 - return map[string]any{ 530 - "ok": true, 531 - "ts": time.Now().UTC().Format(time.RFC3339), 532 - }, "", "" 533 - case "agent.capabilities.get": 534 - return map[string]any{ 535 - "protocol_min": ProtocolVersionV1, 536 - "protocol_max": ProtocolVersionV1, 537 - "capabilities": []string{CapabilityDataPushV1}, 538 - "allowed_methods": append([]string(nil), allowedMethodsV1...), 539 - }, "", "" 540 - case "agent.data.push": 541 - var params rpcDataPushParams 542 - if err := decodeRPCParams(req.Params, &params); err != nil { 543 - return nil, ErrInvalidParamsSymbol, err.Error() 544 - } 545 - params.Topic = strings.TrimSpace(params.Topic) 546 - params.ContentType = strings.TrimSpace(params.ContentType) 547 - params.PayloadBase64 = strings.TrimSpace(params.PayloadBase64) 548 - params.IdempotencyKey = strings.TrimSpace(params.IdempotencyKey) 549 - 550 - if params.Topic == "" { 551 - return nil, ErrInvalidParamsSymbol, "topic is required" 552 - } 553 - if params.ContentType == "" { 554 - return nil, ErrInvalidParamsSymbol, "content_type is required" 555 - } 556 - if params.PayloadBase64 == "" { 557 - return nil, ErrInvalidParamsSymbol, "payload_base64 is required" 558 - } 559 - if params.IdempotencyKey == "" { 560 - return nil, ErrInvalidParamsSymbol, "idempotency_key is required" 561 - } 562 - now := time.Now().UTC() 563 - if !n.allowDataPush(fromPeerID, now) { 564 - limit := n.opts.DataPushPerMinute 565 - if limit <= 0 { 566 - limit = DefaultDataPushRateLimit 567 - } 568 - return nil, ErrRateLimitedSymbol, fmt.Sprintf("peer exceeded rate limit: %d/min", limit) 569 - } 570 - 571 - payloadBytes, err := base64.RawURLEncoding.DecodeString(params.PayloadBase64) 572 - if err != nil { 573 - return nil, ErrInvalidParamsSymbol, "payload_base64 decode failed" 574 - } 575 - if len(payloadBytes) > n.opts.MaxPayloadBytes { 576 - return nil, ErrPayloadTooLargeSymbol, "payload exceeds max_payload_bytes" 577 - } 578 - sessionID, replyTo, err := extractAndValidateSessionForTopic(params.Topic, params.ContentType, payloadBytes) 579 - if err != nil { 580 - return nil, ErrInvalidParamsSymbol, err.Error() 581 - } 582 - 583 - deduped := false 584 - if _, exists, err := n.store.GetDedupeRecord(context.Background(), fromPeerID, params.Topic, params.IdempotencyKey); err != nil { 585 - n.opts.Logger.Warn("dedupe lookup failed", "peer_id", fromPeerID, "err", err) 586 - } else if exists { 587 - deduped = true 588 - } 589 - if !deduped { 590 - record := DedupeRecord{ 591 - FromPeerID: fromPeerID, 592 - Topic: params.Topic, 593 - IdempotencyKey: params.IdempotencyKey, 594 - CreatedAt: now, 595 - ExpiresAt: now.Add(n.opts.DedupeTTL), 596 - } 597 - if err := n.store.PutDedupeRecord(context.Background(), record); err != nil { 598 - n.opts.Logger.Warn("dedupe save failed", "peer_id", fromPeerID, "err", err) 599 - } 600 - if _, err := n.store.PruneDedupeRecords(context.Background(), now, n.opts.DedupeMaxEntries); err != nil { 601 - n.opts.Logger.Warn("dedupe prune failed", "peer_id", fromPeerID, "err", err) 602 - } 603 - } 604 - 605 - if !deduped { 606 - inboxMessage := InboxMessage{ 607 - MessageID: uuid.NewString(), 608 - FromPeerID: fromPeerID, 609 - Topic: params.Topic, 610 - ContentType: params.ContentType, 611 - PayloadBase64: params.PayloadBase64, 612 - IdempotencyKey: params.IdempotencyKey, 613 - SessionID: sessionID, 614 - ReplyTo: replyTo, 615 - ReceivedAt: now, 616 - } 617 - if err := n.store.AppendInboxMessage(context.Background(), inboxMessage); err != nil { 618 - n.opts.Logger.Warn("append inbox message failed", "peer_id", fromPeerID, "err", err) 619 - } 620 - 621 - event := DataPushEvent{ 622 - FromPeerID: fromPeerID, 623 - Topic: params.Topic, 624 - ContentType: params.ContentType, 625 - PayloadBase64: params.PayloadBase64, 626 - PayloadBytes: payloadBytes, 627 - IdempotencyKey: params.IdempotencyKey, 628 - SessionID: sessionID, 629 - ReplyTo: replyTo, 630 - ReceivedAt: now, 631 - Deduped: false, 632 - } 633 - if n.opts.OnDataPush != nil { 634 - n.opts.OnDataPush(event) 635 - } 636 - } 637 - return rpcDataPushResult{Accepted: true, Deduped: deduped}, "", "" 638 - default: 639 - return nil, ErrMethodNotAllowedSymbol, "method=" + req.Method 640 - } 641 - } 642 - 643 - func (n *Node) writeRPCSuccess(stream network.Stream, id any, result any) (int, error) { 644 - payload, err := makeRPCSuccess(id, result) 645 - if err != nil { 646 - return 0, err 647 - } 648 - if len(payload) > n.opts.MaxRPCRequestBytes { 649 - return 0, WrapProtocolError(ErrPayloadTooLarge, "rpc success response too large") 650 - } 651 - written, err := stream.Write(payload) 652 - if err != nil { 653 - return written, err 654 - } 655 - return written, nil 656 - } 657 - 658 - func (n *Node) writeRPCError(stream network.Stream, id any, symbol string, details string) (int, error) { 659 - payload, err := makeRPCError(id, symbol, details) 660 - if err != nil { 661 - return 0, err 662 - } 663 - written, err := stream.Write(payload) 664 - if err != nil { 665 - return written, err 666 - } 667 - return written, nil 668 - } 669 - 670 - func (n *Node) ensurePeerAllowed(ctx context.Context, peerID string) (Contact, error) { 671 - contact, ok, err := n.svc.GetContactByPeerID(ctx, peerID) 672 - if err != nil { 673 - return Contact{}, err 674 - } 675 - if !ok { 676 - return Contact{}, WrapProtocolError(ErrUnauthorized, "peer is not in contacts") 677 - } 678 - switch contact.TrustState { 679 - case TrustStateRevoked, TrustStateConflicted: 680 - return Contact{}, WrapProtocolError(ErrUnauthorized, "peer trust_state=%s", contact.TrustState) 681 - default: 682 - return contact, nil 683 - } 684 - } 685 - 686 - func (n *Node) resolveDialTarget(ctx context.Context, peerID string, addresses []string) (peer.ID, []string, Contact, error) { 687 - peerID = strings.TrimSpace(peerID) 688 - if peerID == "" { 689 - return "", nil, Contact{}, WrapProtocolError(ErrInvalidParams, "peer_id is required") 690 - } 691 - 692 - contact, err := n.ensurePeerAllowed(ctx, peerID) 693 - if err != nil { 694 - return "", nil, Contact{}, err 695 - } 696 - 697 - expectedPeerID, err := peer.Decode(peerID) 698 - if err != nil { 699 - return "", nil, Contact{}, WrapProtocolError(ErrInvalidParams, "invalid peer_id: %v", err) 700 - } 701 - 702 - dialAddresses := normalizeAddresses(addresses) 703 - if len(dialAddresses) == 0 { 704 - dialAddresses = normalizeAddresses(contact.Addresses) 705 - } 706 - if len(dialAddresses) == 0 { 707 - return "", nil, Contact{}, WrapProtocolError(ErrInvalidParams, "no dial addresses available") 708 - } 709 - for _, addr := range dialAddresses { 710 - if err := validateAddressMatchesPeerID(addr, expectedPeerID); err != nil { 711 - return "", nil, Contact{}, err 712 - } 713 - } 714 - return expectedPeerID, dialAddresses, contact, nil 715 - } 716 - 717 - func (n *Node) connect(ctx context.Context, targetPeerID peer.ID, addresses []string) error { 718 - directAddrs, relayAddrs := splitDialAddresses(addresses) 719 - orderedSets := [][]string{directAddrs, relayAddrs} 720 - setLabels := []string{"direct", "relay"} 721 - 722 - connectErrors := make([]string, 0, len(addresses)) 723 - for i, addressSet := range orderedSets { 724 - for _, raw := range addressSet { 725 - addressCtx, cancel := withTimeoutIfNeeded(ctx, n.opts.DialAddrTimeout) 726 - err := n.connectOneAddress(addressCtx, targetPeerID, raw) 727 - cancel() 728 - if err == nil { 729 - return nil 730 - } 731 - connectErrors = append(connectErrors, fmt.Sprintf("%s(%s): %v", setLabels[i], raw, err)) 732 - } 733 - } 734 - if len(connectErrors) == 0 { 735 - return fmt.Errorf("connect to %s failed: no dial addresses", targetPeerID.String()) 736 - } 737 - return fmt.Errorf("connect to %s failed: %s", targetPeerID.String(), strings.Join(connectErrors, "; ")) 738 - } 739 - 740 - func (n *Node) connectOneAddress(ctx context.Context, targetPeerID peer.ID, address string) error { 741 - addr, err := ma.NewMultiaddr(address) 742 - if err != nil { 743 - return fmt.Errorf("invalid dial multiaddr %q: %w", address, err) 744 - } 745 - info := peer.AddrInfo{ 746 - ID: targetPeerID, 747 - Addrs: []ma.Multiaddr{addr}, 748 - } 749 - if err := n.host.Connect(ctx, info); err != nil { 750 - return err 751 - } 752 - return nil 753 - } 754 - 755 - func splitDialAddresses(addresses []string) ([]string, []string) { 756 - direct := make([]string, 0, len(addresses)) 757 - relay := make([]string, 0, len(addresses)) 758 - for _, raw := range addresses { 759 - if strings.Contains(strings.ToLower(strings.TrimSpace(raw)), "/p2p-circuit") { 760 - relay = append(relay, raw) 761 - } else { 762 - direct = append(direct, raw) 763 - } 764 - } 765 - return direct, relay 766 - } 767 - 768 - func (n *Node) hasFreshSession(peerID string) bool { 769 - n.mu.RLock() 770 - defer n.mu.RUnlock() 771 - session, ok := n.sessions[strings.TrimSpace(peerID)] 772 - if !ok { 773 - return false 774 - } 775 - if session.NegotiatedProtocol <= 0 || session.UpdatedAt.IsZero() { 776 - return false 777 - } 778 - return time.Since(session.UpdatedAt) <= sessionFreshWindow 779 - } 780 - 781 - func (n *Node) allowDataPush(peerID string, now time.Time) bool { 782 - limit := n.opts.DataPushPerMinute 783 - if limit <= 0 { 784 - return true 785 - } 786 - if now.IsZero() { 787 - now = time.Now().UTC() 788 - } 789 - windowMinute := now.UTC().Truncate(time.Minute) 790 - peerID = strings.TrimSpace(peerID) 791 - 792 - n.rateMu.Lock() 793 - defer n.rateMu.Unlock() 794 - 795 - window := n.pushRateWindows[peerID] 796 - if window.WindowMinute.IsZero() || !window.WindowMinute.Equal(windowMinute) { 797 - window = pushRateWindow{WindowMinute: windowMinute, Count: 0} 798 - } 799 - if window.Count >= limit { 800 - n.pushRateWindows[peerID] = window 801 - return false 802 - } 803 - window.Count++ 804 - n.pushRateWindows[peerID] = window 805 - return true 806 - } 807 - 808 - func (n *Node) recordSession(ctx context.Context, result HelloResult) error { 809 - peerID := strings.TrimSpace(result.RemotePeerID) 810 - if peerID == "" { 811 - return fmt.Errorf("empty remote peer id") 812 - } 813 - now := time.Now().UTC() 814 - result.UpdatedAt = now 815 - n.mu.Lock() 816 - n.sessions[peerID] = result 817 - n.mu.Unlock() 818 - 819 - oldHistory, foundOld, err := n.store.GetProtocolHistory(ctx, peerID) 820 - if err != nil { 821 - return err 822 - } 823 - if foundOld { 824 - if result.RemoteMaxProtocol < oldHistory.LastRemoteMaxProtocol { 825 - n.opts.Logger.Warn("maep downgrade suspected: remote_max_protocol decreased", "peer_id", peerID, "previous", oldHistory.LastRemoteMaxProtocol, "current", result.RemoteMaxProtocol) 826 - } 827 - if result.NegotiatedProtocol < oldHistory.LastNegotiatedProtocol { 828 - n.opts.Logger.Warn("maep downgrade suspected: negotiated_protocol decreased", "peer_id", peerID, "previous", oldHistory.LastNegotiatedProtocol, "current", result.NegotiatedProtocol) 829 - } 830 - } 831 - history := ProtocolHistory{ 832 - PeerID: peerID, 833 - LastRemoteMaxProtocol: result.RemoteMaxProtocol, 834 - LastNegotiatedProtocol: result.NegotiatedProtocol, 835 - UpdatedAt: now, 836 - } 837 - return n.store.PutProtocolHistory(ctx, history) 838 - } 839 - 840 - func parseHelloMessage(raw []byte) (helloMessage, error) { 841 - var msg helloMessage 842 - if err := decodeStrictJSON(raw, &msg); err != nil { 843 - return helloMessage{}, err 844 - } 845 - msg.Type = strings.TrimSpace(msg.Type) 846 - if msg.Type != "" && msg.Type != "hello" { 847 - return helloMessage{}, WrapProtocolError(ErrInvalidParams, "hello.type must be \"hello\"") 848 - } 849 - if msg.ProtocolMin <= 0 || msg.ProtocolMax <= 0 { 850 - return helloMessage{}, WrapProtocolError(ErrInvalidParams, "hello protocol range must be positive") 851 - } 852 - if msg.ProtocolMin > msg.ProtocolMax { 853 - return helloMessage{}, WrapProtocolError(ErrInvalidParams, "hello protocol_min cannot exceed protocol_max") 854 - } 855 - if len(msg.Capabilities) == 0 { 856 - msg.Capabilities = []string{} 857 - } 858 - return msg, nil 859 - } 860 - 861 - func (n *Node) localHelloMessage() helloMessage { 862 - return helloMessage{ 863 - Type: "hello", 864 - ProtocolMin: ProtocolVersionV1, 865 - ProtocolMax: ProtocolVersionV1, 866 - Capabilities: []string{CapabilityDataPushV1}, 867 - } 868 - } 869 - 870 - func negotiateProtocol(localMin int, localMax int, remoteMin int, remoteMax int) (int, error) { 871 - negotiated := localMax 872 - if remoteMax < negotiated { 873 - negotiated = remoteMax 874 - } 875 - requiredMin := localMin 876 - if remoteMin > requiredMin { 877 - requiredMin = remoteMin 878 - } 879 - if negotiated < requiredMin { 880 - return 0, WrapProtocolError(ErrUnsupportedProtocol, "no protocol overlap") 881 - } 882 - return negotiated, nil 883 - } 884 - 885 - func verifyRemotePeerOnStream(stream network.Stream, expected peer.ID) error { 886 - actual := stream.Conn().RemotePeer() 887 - if actual != expected { 888 - return WrapProtocolError(ErrPeerIDMismatch, "remote peer mismatch: expected=%s actual=%s", expected.String(), actual.String()) 889 - } 890 - remotePub := stream.Conn().RemotePublicKey() 891 - if remotePub == nil { 892 - return WrapProtocolError(ErrPeerIDMismatch, "remote public key is missing") 893 - } 894 - derived, err := peer.IDFromPublicKey(remotePub) 895 - if err != nil { 896 - return WrapProtocolError(ErrPeerIDMismatch, "derive peer id from remote public key failed: %v", err) 897 - } 898 - if derived != expected { 899 - return WrapProtocolError(ErrPeerIDMismatch, "remote public key peer id mismatch") 900 - } 901 - return nil 902 - } 903 - 904 - func readAllLimited(reader io.Reader, maxBytes int) ([]byte, bool, error) { 905 - if maxBytes <= 0 { 906 - maxBytes = MaxRPCRequestBytesV1 907 - } 908 - limited := io.LimitReader(reader, int64(maxBytes)+1) 909 - data, err := io.ReadAll(limited) 910 - if err != nil { 911 - return nil, false, err 912 - } 913 - if len(data) > maxBytes { 914 - return data, true, nil 915 - } 916 - return data, false, nil 917 - } 918 - 919 - func extractAndValidateSessionForTopic(topic string, contentType string, payloadBytes []byte) (string, string, error) { 920 - contentType = strings.ToLower(strings.TrimSpace(contentType)) 921 - if !strings.HasPrefix(contentType, "application/json") { 922 - return "", "", fmt.Errorf("content_type must be application/json") 923 - } 924 - var envelope map[string]any 925 - if err := decodeStrictJSON(payloadBytes, &envelope); err != nil { 926 - return "", "", fmt.Errorf("invalid envelope json: %v", err) 927 - } 928 - if _, err := readRequiredEnvelopeString(envelope, "message_id"); err != nil { 929 - return "", "", err 930 - } 931 - if _, err := readRequiredEnvelopeString(envelope, "text"); err != nil { 932 - return "", "", err 933 - } 934 - sentAt, err := readRequiredEnvelopeString(envelope, "sent_at") 935 - if err != nil { 936 - return "", "", err 937 - } 938 - if _, err := time.Parse(time.RFC3339, sentAt); err != nil { 939 - return "", "", fmt.Errorf("sent_at must be RFC3339") 940 - } 941 - 942 - sessionID, err := readOptionalEnvelopeString(envelope, "session_id") 943 - if err != nil { 944 - return "", "", err 945 - } 946 - replyTo, err := readOptionalEnvelopeString(envelope, "reply_to") 947 - if err != nil { 948 - return "", "", err 949 - } 950 - 951 - if IsDialogueTopic(topic) && sessionID == "" { 952 - return "", "", fmt.Errorf("session_id is required for dialogue topics") 953 - } 954 - if sessionID != "" { 955 - if err := validateSessionID(sessionID); err != nil { 956 - return "", "", err 957 - } 958 - } 959 - return sessionID, replyTo, nil 960 - } 961 - 962 - func readRequiredEnvelopeString(envelope map[string]any, key string) (string, error) { 963 - raw, ok := envelope[key] 964 - if !ok { 965 - return "", fmt.Errorf("%s is required in envelope", key) 966 - } 967 - value, ok := raw.(string) 968 - if !ok { 969 - return "", fmt.Errorf("%s must be string in envelope", key) 970 - } 971 - value = strings.TrimSpace(value) 972 - if value == "" { 973 - return "", fmt.Errorf("%s must be non-empty in envelope", key) 974 - } 975 - return value, nil 976 - } 977 - 978 - func readOptionalEnvelopeString(envelope map[string]any, key string) (string, error) { 979 - raw, ok := envelope[key] 980 - if !ok { 981 - return "", nil 982 - } 983 - value, ok := raw.(string) 984 - if !ok { 985 - return "", fmt.Errorf("%s must be string in envelope", key) 986 - } 987 - return strings.TrimSpace(value), nil 988 - } 989 - 990 - func validateSessionID(sessionID string) error { 991 - id, err := uuid.Parse(strings.TrimSpace(sessionID)) 992 - if err != nil { 993 - return fmt.Errorf("session_id must be uuid_v7") 994 - } 995 - if id.Version() != uuid.Version(7) { 996 - return fmt.Errorf("session_id must be uuid_v7") 997 - } 998 - return nil 999 - } 1000 - 1001 - func protocolErrorFromSymbol(symbol string, details string) error { 1002 - symbol = strings.TrimSpace(symbol) 1003 - details = strings.TrimSpace(details) 1004 - base := protocolErrorBySymbol(symbol) 1005 - if details == "" { 1006 - return base 1007 - } 1008 - return WrapProtocolError(base, "%s", details) 1009 - } 1010 - 1011 - func protocolErrorBySymbol(symbol string) *ProtocolError { 1012 - switch symbol { 1013 - case ErrUnauthorizedSymbol: 1014 - return ErrUnauthorized 1015 - case ErrPeerIDMismatchSymbol: 1016 - return ErrPeerIDMismatch 1017 - case ErrContactConflictedSymbol: 1018 - return ErrContactConflicted 1019 - case ErrMethodNotAllowedSymbol: 1020 - return ErrMethodNotAllowed 1021 - case ErrPayloadTooLargeSymbol: 1022 - return ErrPayloadTooLarge 1023 - case ErrRateLimitedSymbol: 1024 - return ErrRateLimited 1025 - case ErrUnsupportedProtocolSymbol: 1026 - return ErrUnsupportedProtocol 1027 - case ErrInvalidJSONProfileSymbol: 1028 - return ErrInvalidJSONProfile 1029 - case ErrInvalidContactCardSymbol: 1030 - return ErrInvalidContactCard 1031 - case ErrInvalidParamsSymbol: 1032 - return ErrInvalidParams 1033 - default: 1034 - return &ProtocolError{Symbol: symbol, Message: symbol} 1035 - } 1036 - } 1037 - 1038 - func normalizeNodeOptions(opts NodeOptions) NodeOptions { 1039 - if opts.DialAddrTimeout <= 0 { 1040 - opts.DialAddrTimeout = DefaultDialAddrTimeout 1041 - } 1042 - if opts.HelloTimeout <= 0 { 1043 - opts.HelloTimeout = DefaultHelloTimeout 1044 - } 1045 - if opts.RPCTimeout <= 0 { 1046 - opts.RPCTimeout = DefaultRPCTimeout 1047 - } 1048 - if opts.MaxRPCRequestBytes <= 0 { 1049 - opts.MaxRPCRequestBytes = MaxRPCRequestBytesV1 1050 - } 1051 - if opts.MaxPayloadBytes <= 0 { 1052 - opts.MaxPayloadBytes = MaxPayloadBytesV1 1053 - } 1054 - if opts.DataPushPerMinute <= 0 { 1055 - opts.DataPushPerMinute = DefaultDataPushRateLimit 1056 - } 1057 - if opts.DedupeTTL <= 0 { 1058 - opts.DedupeTTL = DefaultDedupeTTL 1059 - } 1060 - if opts.DedupeMaxEntries <= 0 { 1061 - opts.DedupeMaxEntries = DefaultDedupeMaxEntries 1062 - } 1063 - if opts.Logger == nil { 1064 - opts.Logger = slog.Default() 1065 - } 1066 - if !opts.DialOnly { 1067 - opts.ListenAddrs = normalizeAddresses(opts.ListenAddrs) 1068 - if len(opts.ListenAddrs) == 0 { 1069 - opts.ListenAddrs = []string{ 1070 - "/ip4/0.0.0.0/udp/0/quic-v1", 1071 - "/ip4/0.0.0.0/tcp/0", 1072 - } 1073 - } 1074 - } 1075 - return opts 1076 - } 1077 - 1078 - func withTimeoutIfNeeded(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { 1079 - if timeout <= 0 { 1080 - return context.WithCancel(ctx) 1081 - } 1082 - return context.WithTimeout(ctx, timeout) 1083 - } 1084 - 1085 - func sortStrings(values []string) { 1086 - if len(values) <= 1 { 1087 - return 1088 - } 1089 - for i := 0; i < len(values)-1; i++ { 1090 - for j := i + 1; j < len(values); j++ { 1091 - if values[j] < values[i] { 1092 - values[i], values[j] = values[j], values[i] 1093 - } 1094 - } 1095 - } 1096 - }
-28
maep/node_rate_limit_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "testing" 5 - "time" 6 - ) 7 - 8 - func TestNodeAllowDataPushPerMinute(t *testing.T) { 9 - n := &Node{ 10 - opts: NodeOptions{ 11 - DataPushPerMinute: 2, 12 - }, 13 - pushRateWindows: map[string]pushRateWindow{}, 14 - } 15 - now := time.Date(2026, 2, 6, 12, 30, 10, 0, time.UTC) 16 - if !n.allowDataPush("peer-a", now) { 17 - t.Fatalf("first request should pass") 18 - } 19 - if !n.allowDataPush("peer-a", now.Add(10*time.Second)) { 20 - t.Fatalf("second request should pass") 21 - } 22 - if n.allowDataPush("peer-a", now.Add(20*time.Second)) { 23 - t.Fatalf("third request in same minute should be rate limited") 24 - } 25 - if !n.allowDataPush("peer-a", now.Add(1*time.Minute)) { 26 - t.Fatalf("request in next minute should pass") 27 - } 28 - }
-18
maep/node_retry_test.go
··· 1 - package maep 2 - 3 - import "testing" 4 - 5 - func TestShouldRetryAfterUnsupported(t *testing.T) { 6 - if !shouldRetryAfterUnsupported("ERR_UNSUPPORTED_PROTOCOL", false) { 7 - t.Fatalf("expected retry for unsupported protocol on first attempt") 8 - } 9 - if !shouldRetryAfterUnsupported(" err_unsupported_protocol ", false) { 10 - t.Fatalf("expected case-insensitive retry for unsupported protocol") 11 - } 12 - if shouldRetryAfterUnsupported("ERR_METHOD_NOT_ALLOWED", false) { 13 - t.Fatalf("unexpected retry for unrelated protocol symbol") 14 - } 15 - if shouldRetryAfterUnsupported("ERR_UNSUPPORTED_PROTOCOL", true) { 16 - t.Fatalf("unexpected retry after already retried") 17 - } 18 - }
-96
maep/node_session_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "encoding/json" 5 - "testing" 6 - ) 7 - 8 - func TestExtractAndValidateSessionForTopic_DialogueRequiresSessionID(t *testing.T) { 9 - payload := map[string]any{ 10 - "message_id": "msg_1", 11 - "text": "hello", 12 - "sent_at": "2026-02-08T01:02:03Z", 13 - } 14 - raw, _ := json.Marshal(payload) 15 - 16 - _, _, err := extractAndValidateSessionForTopic("dm.reply.v1", "application/json", raw) 17 - if err == nil { 18 - t.Fatalf("expected error when dialogue topic is missing session_id") 19 - } 20 - } 21 - 22 - func TestExtractAndValidateSessionForTopic_DialogueAcceptsSessionID(t *testing.T) { 23 - payload := map[string]any{ 24 - "message_id": "msg_2", 25 - "text": "hello", 26 - "sent_at": "2026-02-08T01:02:03Z", 27 - "session_id": "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", 28 - "reply_to": "msg_prev", 29 - } 30 - raw, _ := json.Marshal(payload) 31 - 32 - sessionID, replyTo, err := extractAndValidateSessionForTopic("dm.reply.v1", "application/json", raw) 33 - if err != nil { 34 - t.Fatalf("extractAndValidateSessionForTopic() error = %v", err) 35 - } 36 - if sessionID != "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456" { 37 - t.Fatalf("session_id mismatch: got %q", sessionID) 38 - } 39 - if replyTo != "msg_prev" { 40 - t.Fatalf("reply_to mismatch: got %q", replyTo) 41 - } 42 - } 43 - 44 - func TestExtractAndValidateSessionForTopic_NonDialogueAllowsMissingSessionID(t *testing.T) { 45 - payload := map[string]any{ 46 - "message_id": "msg_3", 47 - "text": "hello", 48 - "sent_at": "2026-02-08T01:02:03Z", 49 - } 50 - raw, _ := json.Marshal(payload) 51 - 52 - sessionID, replyTo, err := extractAndValidateSessionForTopic("agent.status.v1", "application/json", raw) 53 - if err != nil { 54 - t.Fatalf("extractAndValidateSessionForTopic() error = %v", err) 55 - } 56 - if sessionID != "" { 57 - t.Fatalf("expected empty session_id, got %q", sessionID) 58 - } 59 - if replyTo != "" { 60 - t.Fatalf("expected empty reply_to, got %q", replyTo) 61 - } 62 - } 63 - 64 - func TestExtractAndValidateSessionForTopic_RejectsNonUUIDv7SessionID(t *testing.T) { 65 - payload := map[string]any{ 66 - "message_id": "msg_4", 67 - "text": "hello", 68 - "sent_at": "2026-02-08T01:02:03Z", 69 - "session_id": "peerA::dialogue.v1", 70 - } 71 - raw, _ := json.Marshal(payload) 72 - 73 - _, _, err := extractAndValidateSessionForTopic("dm.reply.v1", "application/json", raw) 74 - if err == nil { 75 - t.Fatalf("expected error for non-uuid_v7 session_id") 76 - } 77 - } 78 - 79 - func TestExtractAndValidateSessionForTopic_RejectsPlainTextContentType(t *testing.T) { 80 - _, _, err := extractAndValidateSessionForTopic("dm.reply.v1", "text/plain", []byte("hello")) 81 - if err == nil { 82 - t.Fatalf("expected error for plain-text content type") 83 - } 84 - } 85 - 86 - func TestExtractAndValidateSessionForTopic_RequiresEnvelopeFields(t *testing.T) { 87 - payload := map[string]any{ 88 - "text": "hello", 89 - "sent_at": "2026-02-08T01:02:03Z", 90 - } 91 - raw, _ := json.Marshal(payload) 92 - _, _, err := extractAndValidateSessionForTopic("agent.status.v1", "application/json", raw) 93 - if err == nil { 94 - t.Fatalf("expected error for missing message_id") 95 - } 96 - }
-235
maep/rpc.go
··· 1 - package maep 2 - 3 - import ( 4 - "bytes" 5 - "encoding/json" 6 - "fmt" 7 - "strings" 8 - 9 - "github.com/google/uuid" 10 - ) 11 - 12 - var allowedMethodsV1 = []string{ 13 - "agent.ping", 14 - "agent.capabilities.get", 15 - "agent.data.push", 16 - } 17 - 18 - var rpcErrorCodeBySymbol = map[string]int{ 19 - ErrUnauthorizedSymbol: -32001, 20 - ErrPeerIDMismatchSymbol: -32002, 21 - ErrContactConflictedSymbol: -32003, 22 - ErrMethodNotAllowedSymbol: -32004, 23 - ErrPayloadTooLargeSymbol: -32005, 24 - ErrRateLimitedSymbol: -32006, 25 - ErrUnsupportedProtocolSymbol: -32007, 26 - ErrInvalidJSONProfileSymbol: -32008, 27 - ErrInvalidContactCardSymbol: -32009, 28 - ErrInvalidParamsSymbol: -32602, 29 - } 30 - 31 - type rpcRequest struct { 32 - JSONRPC string 33 - ID any 34 - HasID bool 35 - Method string 36 - Params json.RawMessage 37 - } 38 - 39 - type rpcErrorObject struct { 40 - Code int `json:"code"` 41 - Message string `json:"message"` 42 - Data map[string]any `json:"data,omitempty"` 43 - } 44 - 45 - type rpcResponse struct { 46 - JSONRPC string `json:"jsonrpc"` 47 - ID any `json:"id,omitempty"` 48 - Result any `json:"result,omitempty"` 49 - Error *rpcErrorObject `json:"error,omitempty"` 50 - } 51 - 52 - type rpcDataPushParams struct { 53 - Topic string `json:"topic"` 54 - ContentType string `json:"content_type"` 55 - PayloadBase64 string `json:"payload_base64"` 56 - IdempotencyKey string `json:"idempotency_key"` 57 - } 58 - 59 - type rpcDataPushResult struct { 60 - Accepted bool `json:"accepted"` 61 - Deduped bool `json:"deduped"` 62 - } 63 - 64 - func parseRPCRequest(raw []byte) (rpcRequest, error) { 65 - var obj map[string]any 66 - if err := decodeStrictJSON(raw, &obj); err != nil { 67 - return rpcRequest{}, err 68 - } 69 - 70 - jsonrpcValue, ok := obj["jsonrpc"] 71 - if !ok { 72 - return rpcRequest{}, WrapProtocolError(ErrInvalidParams, "jsonrpc is required") 73 - } 74 - jsonrpcString, ok := jsonrpcValue.(string) 75 - if !ok || strings.TrimSpace(jsonrpcString) != JSONRPCVersion { 76 - return rpcRequest{}, WrapProtocolError(ErrInvalidParams, "jsonrpc must be %q", JSONRPCVersion) 77 - } 78 - 79 - methodValue, ok := obj["method"] 80 - if !ok { 81 - return rpcRequest{}, WrapProtocolError(ErrInvalidParams, "method is required") 82 - } 83 - methodString, ok := methodValue.(string) 84 - if !ok || strings.TrimSpace(methodString) == "" { 85 - return rpcRequest{}, WrapProtocolError(ErrInvalidParams, "method must be a non-empty string") 86 - } 87 - 88 - paramsRaw := json.RawMessage([]byte("{}")) 89 - if paramsValue, ok := obj["params"]; ok { 90 - rawBytes, err := json.Marshal(paramsValue) 91 - if err != nil { 92 - return rpcRequest{}, WrapProtocolError(ErrInvalidParams, "params marshal failed: %v", err) 93 - } 94 - paramsRaw = rawBytes 95 - } 96 - 97 - req := rpcRequest{ 98 - JSONRPC: jsonrpcString, 99 - Method: strings.TrimSpace(methodString), 100 - Params: paramsRaw, 101 - } 102 - 103 - if idValue, ok := obj["id"]; ok { 104 - if !isValidRPCID(idValue) { 105 - return rpcRequest{}, WrapProtocolError(ErrInvalidParams, "id must be string or integer") 106 - } 107 - req.HasID = true 108 - req.ID = idValue 109 - } 110 - return req, nil 111 - } 112 - 113 - func isValidRPCID(v any) bool { 114 - switch x := v.(type) { 115 - case string: 116 - return strings.TrimSpace(x) != "" 117 - case json.Number: 118 - return isStrictInteger(x.String()) 119 - default: 120 - return false 121 - } 122 - } 123 - 124 - func decodeRPCParams(raw json.RawMessage, out any) error { 125 - if len(bytes.TrimSpace(raw)) == 0 { 126 - return WrapProtocolError(ErrInvalidParams, "params is required") 127 - } 128 - if err := decodeStrictJSON(raw, out); err != nil { 129 - return WrapProtocolError(ErrInvalidParams, "params decode failed: %v", err) 130 - } 131 - return nil 132 - } 133 - 134 - func makeRPCSuccess(id any, result any) ([]byte, error) { 135 - resp := rpcResponse{ 136 - JSONRPC: JSONRPCVersion, 137 - ID: id, 138 - Result: result, 139 - } 140 - return json.Marshal(resp) 141 - } 142 - 143 - func makeRPCError(id any, symbol string, details string) ([]byte, error) { 144 - code, ok := rpcErrorCodeBySymbol[symbol] 145 - if !ok { 146 - code = -32000 147 - } 148 - errObj := &rpcErrorObject{ 149 - Code: code, 150 - Message: symbol, 151 - } 152 - if strings.TrimSpace(details) != "" { 153 - errObj.Data = map[string]any{"details": details} 154 - } 155 - resp := rpcResponse{ 156 - JSONRPC: JSONRPCVersion, 157 - ID: id, 158 - Error: errObj, 159 - } 160 - return json.Marshal(resp) 161 - } 162 - 163 - func parseRPCResponse(raw []byte) (json.RawMessage, string, string, error) { 164 - var obj map[string]any 165 - if err := decodeStrictJSON(raw, &obj); err != nil { 166 - return nil, "", "", err 167 - } 168 - 169 - jsonrpcValue, ok := obj["jsonrpc"] 170 - if !ok { 171 - return nil, "", "", fmt.Errorf("response missing jsonrpc") 172 - } 173 - jsonrpcString, ok := jsonrpcValue.(string) 174 - if !ok || strings.TrimSpace(jsonrpcString) != JSONRPCVersion { 175 - return nil, "", "", fmt.Errorf("response jsonrpc must be %q", JSONRPCVersion) 176 - } 177 - 178 - if errValue, ok := obj["error"]; ok { 179 - errMap, ok := errValue.(map[string]any) 180 - if !ok { 181 - return nil, "", "", fmt.Errorf("response error must be object") 182 - } 183 - message, _ := errMap["message"].(string) 184 - details := "" 185 - if dataValue, ok := errMap["data"].(map[string]any); ok { 186 - if detailsValue, ok := dataValue["details"].(string); ok { 187 - details = detailsValue 188 - } 189 - } 190 - return nil, strings.TrimSpace(message), details, nil 191 - } 192 - 193 - resultValue, ok := obj["result"] 194 - if !ok { 195 - return nil, "", "", fmt.Errorf("response missing result") 196 - } 197 - resultRaw, err := json.Marshal(resultValue) 198 - if err != nil { 199 - return nil, "", "", fmt.Errorf("marshal response result: %w", err) 200 - } 201 - return resultRaw, "", "", nil 202 - } 203 - 204 - func generateRPCRequestID() string { 205 - return "req_" + uuid.NewString() 206 - } 207 - 208 - func isAllowedMethod(method string) bool { 209 - for _, candidate := range allowedMethodsV1 { 210 - if method == candidate { 211 - return true 212 - } 213 - } 214 - return false 215 - } 216 - 217 - // Best-effort extraction of request id for parse-error responses. 218 - // This intentionally uses non-strict JSON decode so we can still reply when 219 - // semantic validation fails but a valid id is present. 220 - func extractRPCIDForError(raw []byte) (any, bool) { 221 - if len(bytes.TrimSpace(raw)) == 0 { 222 - return nil, false 223 - } 224 - dec := json.NewDecoder(bytes.NewReader(raw)) 225 - dec.UseNumber() 226 - var obj map[string]any 227 - if err := dec.Decode(&obj); err != nil { 228 - return nil, false 229 - } 230 - idValue, ok := obj["id"] 231 - if !ok || !isValidRPCID(idValue) { 232 - return nil, false 233 - } 234 - return idValue, true 235 - }
-65
maep/rpc_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "encoding/json" 5 - "testing" 6 - ) 7 - 8 - func TestExtractRPCIDForError_StringID(t *testing.T) { 9 - raw := []byte(`{"jsonrpc":"2.0","id":"req-1","method":"agent.ping","params":{}}`) 10 - id, ok := extractRPCIDForError(raw) 11 - if !ok { 12 - t.Fatalf("expected id to be extracted") 13 - } 14 - got, typeOK := id.(string) 15 - if !typeOK { 16 - t.Fatalf("expected string id, got %T", id) 17 - } 18 - if got != "req-1" { 19 - t.Fatalf("id mismatch: got %q want %q", got, "req-1") 20 - } 21 - } 22 - 23 - func TestExtractRPCIDForError_IntegerID(t *testing.T) { 24 - raw := []byte(`{"jsonrpc":"2.0","id":7,"method":"agent.ping","params":{}}`) 25 - id, ok := extractRPCIDForError(raw) 26 - if !ok { 27 - t.Fatalf("expected id to be extracted") 28 - } 29 - got, typeOK := id.(json.Number) 30 - if !typeOK { 31 - t.Fatalf("expected json.Number id, got %T", id) 32 - } 33 - if got.String() != "7" { 34 - t.Fatalf("id mismatch: got %q want %q", got.String(), "7") 35 - } 36 - } 37 - 38 - func TestExtractRPCIDForError_InvalidID(t *testing.T) { 39 - raw := []byte(`{"jsonrpc":"2.0","id":1.5,"method":"agent.ping","params":{}}`) 40 - if _, ok := extractRPCIDForError(raw); ok { 41 - t.Fatalf("expected invalid id to be rejected") 42 - } 43 - } 44 - 45 - func TestExtractRPCIDForError_BestEffortForSemanticFailure(t *testing.T) { 46 - raw := []byte(`{"jsonrpc":"2.0","id":"req-2","params":null}`) 47 - id, ok := extractRPCIDForError(raw) 48 - if !ok { 49 - t.Fatalf("expected id to be extracted") 50 - } 51 - got, typeOK := id.(string) 52 - if !typeOK { 53 - t.Fatalf("expected string id, got %T", id) 54 - } 55 - if got != "req-2" { 56 - t.Fatalf("id mismatch: got %q want %q", got, "req-2") 57 - } 58 - } 59 - 60 - func TestExtractRPCIDForError_InvalidJSON(t *testing.T) { 61 - raw := []byte(`{"jsonrpc":"2.0","id":"req-3",`) 62 - if _, ok := extractRPCIDForError(raw); ok { 63 - t.Fatalf("expected invalid json to return no id") 64 - } 65 - }
-272
maep/service.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "strings" 8 - "time" 9 - 10 - "github.com/google/uuid" 11 - ) 12 - 13 - type Service struct { 14 - store Store 15 - } 16 - 17 - func NewService(store Store) *Service { 18 - return &Service{store: store} 19 - } 20 - 21 - func (s *Service) EnsureIdentity(ctx context.Context, now time.Time) (Identity, bool, error) { 22 - if s == nil || s.store == nil { 23 - return Identity{}, false, fmt.Errorf("nil maep service") 24 - } 25 - if err := s.store.Ensure(ctx); err != nil { 26 - return Identity{}, false, err 27 - } 28 - 29 - identity, ok, err := s.store.GetIdentity(ctx) 30 - if err != nil { 31 - return Identity{}, false, err 32 - } 33 - if ok { 34 - return identity, false, nil 35 - } 36 - 37 - identity, err = GenerateIdentity(now) 38 - if err != nil { 39 - return Identity{}, false, err 40 - } 41 - if err := s.store.PutIdentity(ctx, identity); err != nil { 42 - return Identity{}, false, err 43 - } 44 - return identity, true, nil 45 - } 46 - 47 - func (s *Service) GetIdentity(ctx context.Context) (Identity, bool, error) { 48 - if s == nil || s.store == nil { 49 - return Identity{}, false, fmt.Errorf("nil maep service") 50 - } 51 - return s.store.GetIdentity(ctx) 52 - } 53 - 54 - func (s *Service) ExportContactCard(ctx context.Context, addresses []string, minProtocol int, maxProtocol int, now time.Time, expiresAt *time.Time) (ContactCard, []byte, error) { 55 - identity, ok, err := s.GetIdentity(ctx) 56 - if err != nil { 57 - return ContactCard{}, nil, err 58 - } 59 - if !ok { 60 - return ContactCard{}, nil, fmt.Errorf("identity is not initialized; run `mistermorph maep init`") 61 - } 62 - 63 - card, err := BuildSignedContactCard(identity, addresses, minProtocol, maxProtocol, now, expiresAt) 64 - if err != nil { 65 - return ContactCard{}, nil, err 66 - } 67 - 68 - raw, err := json.MarshalIndent(card, "", " ") 69 - if err != nil { 70 - return ContactCard{}, nil, fmt.Errorf("marshal contact card: %w", err) 71 - } 72 - raw = append(raw, '\n') 73 - return card, raw, nil 74 - } 75 - 76 - func (s *Service) ImportContactCard(ctx context.Context, rawCard []byte, displayName string, now time.Time) (ImportContactResult, error) { 77 - if s == nil || s.store == nil { 78 - return ImportContactResult{}, fmt.Errorf("nil maep service") 79 - } 80 - if err := s.store.Ensure(ctx); err != nil { 81 - return ImportContactResult{}, err 82 - } 83 - parsed, err := ParseAndVerifyContactCard(rawCard, now) 84 - if err != nil { 85 - return ImportContactResult{}, err 86 - } 87 - payload := parsed.Card.Payload 88 - peerID := strings.TrimSpace(payload.PeerID) 89 - 90 - existingByUUID, foundByUUID, err := s.store.GetContactByNodeUUID(ctx, payload.NodeUUID) 91 - if err != nil { 92 - return ImportContactResult{}, err 93 - } 94 - if foundByUUID && strings.TrimSpace(existingByUUID.PeerID) != peerID { 95 - now = normalizedNow(now) 96 - previousTrust := existingByUUID.TrustState 97 - existingByUUID.TrustState = TrustStateConflicted 98 - existingByUUID.UpdatedAt = now 99 - if err := s.store.PutContact(ctx, existingByUUID); err != nil { 100 - return ImportContactResult{}, err 101 - } 102 - if err := s.appendAuditEvent(ctx, now, AuditActionContactImportConflict, existingByUUID, previousTrust, existingByUUID.TrustState, "node_uuid_conflict", map[string]string{ 103 - "existing_peer_id": existingByUUID.PeerID, 104 - "incoming_peer_id": peerID, 105 - }); err != nil { 106 - return ImportContactResult{}, err 107 - } 108 - return ImportContactResult{Contact: existingByUUID, Conflicted: true}, WrapProtocolError(ErrContactConflicted, "node_uuid already maps to another peer_id") 109 - } 110 - 111 - existingByPeer, foundByPeer, err := s.store.GetContactByPeerID(ctx, peerID) 112 - if err != nil { 113 - return ImportContactResult{}, err 114 - } 115 - if foundByPeer && strings.TrimSpace(existingByPeer.NodeUUID) != "" && strings.TrimSpace(existingByPeer.NodeUUID) != strings.TrimSpace(payload.NodeUUID) { 116 - now = normalizedNow(now) 117 - previousTrust := existingByPeer.TrustState 118 - existingByPeer.TrustState = TrustStateConflicted 119 - existingByPeer.UpdatedAt = now 120 - if err := s.store.PutContact(ctx, existingByPeer); err != nil { 121 - return ImportContactResult{}, err 122 - } 123 - if err := s.appendAuditEvent(ctx, now, AuditActionContactImportConflict, existingByPeer, previousTrust, existingByPeer.TrustState, "peer_id_conflict", map[string]string{ 124 - "existing_node_uuid": existingByPeer.NodeUUID, 125 - "incoming_node_uuid": payload.NodeUUID, 126 - }); err != nil { 127 - return ImportContactResult{}, err 128 - } 129 - return ImportContactResult{Contact: existingByPeer, Conflicted: true}, WrapProtocolError(ErrContactConflicted, "peer_id already maps to another node_uuid") 130 - } 131 - 132 - now = normalizedNow(now) 133 - contact := Contact{ 134 - NodeUUID: payload.NodeUUID, 135 - PeerID: payload.PeerID, 136 - NodeID: payload.NodeID, 137 - DisplayName: strings.TrimSpace(displayName), 138 - IdentityPubEd25519: payload.IdentityPubEd25519, 139 - Addresses: append([]string(nil), payload.Addresses...), 140 - MinSupportedProtocol: payload.MinSupportedProtocol, 141 - MaxSupportedProtocol: payload.MaxSupportedProtocol, 142 - IssuedAt: payload.IssuedAt.UTC(), 143 - ExpiresAt: payload.ExpiresAt, 144 - KeyRotationOf: payload.KeyRotationOf, 145 - CardSigAlg: parsed.Card.SigAlg, 146 - CardSigFormat: parsed.Card.SigFormat, 147 - CardSig: parsed.Card.Sig, 148 - TrustState: TrustStateTOFU, 149 - CreatedAt: now, 150 - UpdatedAt: now, 151 - } 152 - if contact.NodeID == "" { 153 - contact.NodeID = NodeIDFromPeerID(contact.PeerID) 154 - } 155 - 156 - created := true 157 - updated := false 158 - previousTrust := TrustState("") 159 - if foundByPeer { 160 - created = false 161 - updated = true 162 - contact.CreatedAt = existingByPeer.CreatedAt 163 - previousTrust = existingByPeer.TrustState 164 - contact.TrustState = preserveTrustState(existingByPeer.TrustState) 165 - if contact.DisplayName == "" { 166 - contact.DisplayName = existingByPeer.DisplayName 167 - } 168 - contact.LastSeen = existingByPeer.LastSeen 169 - } 170 - 171 - if err := s.store.PutContact(ctx, contact); err != nil { 172 - return ImportContactResult{}, err 173 - } 174 - action := AuditActionContactImportCreated 175 - if updated { 176 - action = AuditActionContactImportUpdated 177 - } 178 - if err := s.appendAuditEvent(ctx, now, action, contact, previousTrust, contact.TrustState, "", nil); err != nil { 179 - return ImportContactResult{}, err 180 - } 181 - 182 - return ImportContactResult{Contact: contact, Created: created, Updated: updated}, nil 183 - } 184 - 185 - func (s *Service) ListContacts(ctx context.Context) ([]Contact, error) { 186 - if s == nil || s.store == nil { 187 - return nil, fmt.Errorf("nil maep service") 188 - } 189 - return s.store.ListContacts(ctx) 190 - } 191 - 192 - func (s *Service) ListInboxMessages(ctx context.Context, fromPeerID string, topic string, limit int) ([]InboxMessage, error) { 193 - if s == nil || s.store == nil { 194 - return nil, fmt.Errorf("nil maep service") 195 - } 196 - return s.store.ListInboxMessages(ctx, fromPeerID, topic, limit) 197 - } 198 - 199 - func (s *Service) ListOutboxMessages(ctx context.Context, toPeerID string, topic string, limit int) ([]OutboxMessage, error) { 200 - if s == nil || s.store == nil { 201 - return nil, fmt.Errorf("nil maep service") 202 - } 203 - return s.store.ListOutboxMessages(ctx, toPeerID, topic, limit) 204 - } 205 - 206 - func (s *Service) ListAuditEvents(ctx context.Context, peerID string, action string, limit int) ([]AuditEvent, error) { 207 - if s == nil || s.store == nil { 208 - return nil, fmt.Errorf("nil maep service") 209 - } 210 - return s.store.ListAuditEvents(ctx, peerID, action, limit) 211 - } 212 - 213 - func (s *Service) GetContactByPeerID(ctx context.Context, peerID string) (Contact, bool, error) { 214 - if s == nil || s.store == nil { 215 - return Contact{}, false, fmt.Errorf("nil maep service") 216 - } 217 - return s.store.GetContactByPeerID(ctx, peerID) 218 - } 219 - 220 - func (s *Service) MarkContactVerified(ctx context.Context, peerID string, now time.Time) (Contact, error) { 221 - contact, ok, err := s.GetContactByPeerID(ctx, peerID) 222 - if err != nil { 223 - return Contact{}, err 224 - } 225 - if !ok { 226 - return Contact{}, fmt.Errorf("contact not found: %s", peerID) 227 - } 228 - if contact.TrustState == TrustStateRevoked || contact.TrustState == TrustStateConflicted { 229 - return Contact{}, WrapProtocolError(ErrContactConflicted, "contact cannot be promoted from state=%s", contact.TrustState) 230 - } 231 - previousTrust := contact.TrustState 232 - contact.TrustState = TrustStateVerified 233 - contact.UpdatedAt = normalizedNow(now) 234 - if err := s.store.PutContact(ctx, contact); err != nil { 235 - return Contact{}, err 236 - } 237 - if err := s.appendAuditEvent(ctx, contact.UpdatedAt, AuditActionTrustStateChanged, contact, previousTrust, contact.TrustState, "manual_verify", nil); err != nil { 238 - return Contact{}, err 239 - } 240 - return contact, nil 241 - } 242 - 243 - func (s *Service) appendAuditEvent(ctx context.Context, now time.Time, action string, contact Contact, previousTrustState TrustState, newTrustState TrustState, reason string, metadata map[string]string) error { 244 - event := AuditEvent{ 245 - EventID: "evt_" + uuid.NewString(), 246 - Action: strings.TrimSpace(action), 247 - PeerID: strings.TrimSpace(contact.PeerID), 248 - NodeUUID: strings.TrimSpace(contact.NodeUUID), 249 - PreviousTrustState: previousTrustState, 250 - NewTrustState: newTrustState, 251 - Reason: strings.TrimSpace(reason), 252 - Metadata: metadata, 253 - CreatedAt: normalizedNow(now), 254 - } 255 - return s.store.AppendAuditEvent(ctx, event) 256 - } 257 - 258 - func preserveTrustState(existing TrustState) TrustState { 259 - switch existing { 260 - case TrustStateVerified, TrustStateConflicted, TrustStateRevoked: 261 - return existing 262 - default: 263 - return TrustStateTOFU 264 - } 265 - } 266 - 267 - func normalizedNow(now time.Time) time.Time { 268 - if now.IsZero() { 269 - return time.Now().UTC() 270 - } 271 - return now.UTC() 272 - }
-82
maep/service_audit_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "path/filepath" 8 - "testing" 9 - "time" 10 - ) 11 - 12 - func TestServiceAuditOnImportAndVerify(t *testing.T) { 13 - ctx := context.Background() 14 - root := filepath.Join(t.TempDir(), "maep") 15 - store := NewFileStore(root) 16 - svc := NewService(store) 17 - 18 - now := time.Date(2026, 2, 6, 12, 0, 0, 0, time.UTC) 19 - remoteIdentity, err := GenerateIdentity(now) 20 - if err != nil { 21 - t.Fatalf("GenerateIdentity() error = %v", err) 22 - } 23 - card, err := BuildSignedContactCard( 24 - remoteIdentity, 25 - []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/4102/p2p/%s", remoteIdentity.PeerID)}, 26 - 1, 27 - 1, 28 - now, 29 - nil, 30 - ) 31 - if err != nil { 32 - t.Fatalf("BuildSignedContactCard() error = %v", err) 33 - } 34 - rawCard, err := json.Marshal(card) 35 - if err != nil { 36 - t.Fatalf("json.Marshal(card) error = %v", err) 37 - } 38 - if _, err := svc.ImportContactCard(ctx, rawCard, "remote", now); err != nil { 39 - t.Fatalf("ImportContactCard() error = %v", err) 40 - } 41 - 42 - if _, err := svc.MarkContactVerified(ctx, remoteIdentity.PeerID, now.Add(time.Minute)); err != nil { 43 - t.Fatalf("MarkContactVerified() error = %v", err) 44 - } 45 - 46 - events, err := svc.ListAuditEvents(ctx, remoteIdentity.PeerID, "", 10) 47 - if err != nil { 48 - t.Fatalf("ListAuditEvents() error = %v", err) 49 - } 50 - if len(events) != 2 { 51 - t.Fatalf("ListAuditEvents() length mismatch: got %d want 2", len(events)) 52 - } 53 - 54 - foundImport := false 55 - foundVerify := false 56 - for _, event := range events { 57 - switch event.Action { 58 - case AuditActionContactImportCreated: 59 - foundImport = true 60 - if event.NewTrustState != TrustStateTOFU { 61 - t.Fatalf("import audit trust state mismatch: got %s want %s", event.NewTrustState, TrustStateTOFU) 62 - } 63 - case AuditActionTrustStateChanged: 64 - foundVerify = true 65 - if event.PreviousTrustState != TrustStateTOFU { 66 - t.Fatalf("verify previous trust mismatch: got %s want %s", event.PreviousTrustState, TrustStateTOFU) 67 - } 68 - if event.NewTrustState != TrustStateVerified { 69 - t.Fatalf("verify new trust mismatch: got %s want %s", event.NewTrustState, TrustStateVerified) 70 - } 71 - if event.Reason != "manual_verify" { 72 - t.Fatalf("verify reason mismatch: got %s want manual_verify", event.Reason) 73 - } 74 - } 75 - } 76 - if !foundImport { 77 - t.Fatalf("missing %s audit event", AuditActionContactImportCreated) 78 - } 79 - if !foundVerify { 80 - t.Fatalf("missing %s audit event", AuditActionTrustStateChanged) 81 - } 82 - }
-40
maep/service_identity_test.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "testing" 6 - "time" 7 - ) 8 - 9 - func TestServiceEnsureIdentity_ReusesExistingIdentity(t *testing.T) { 10 - svc := NewService(NewFileStore(t.TempDir())) 11 - ctx := context.Background() 12 - 13 - first, created, err := svc.EnsureIdentity(ctx, time.Now().UTC()) 14 - if err != nil { 15 - t.Fatalf("EnsureIdentity() first call error = %v", err) 16 - } 17 - if !created { 18 - t.Fatalf("EnsureIdentity() first call expected created=true") 19 - } 20 - 21 - second, created, err := svc.EnsureIdentity(ctx, time.Now().UTC().Add(time.Second)) 22 - if err != nil { 23 - t.Fatalf("EnsureIdentity() second call error = %v", err) 24 - } 25 - if created { 26 - t.Fatalf("EnsureIdentity() second call expected created=false") 27 - } 28 - if second.PeerID != first.PeerID { 29 - t.Fatalf("peer_id changed: got %s want %s", second.PeerID, first.PeerID) 30 - } 31 - if second.NodeUUID != first.NodeUUID { 32 - t.Fatalf("node_uuid changed: got %s want %s", second.NodeUUID, first.NodeUUID) 33 - } 34 - if second.NodeID != first.NodeID { 35 - t.Fatalf("node_id changed: got %s want %s", second.NodeID, first.NodeID) 36 - } 37 - if second.IdentityPubEd25519 != first.IdentityPubEd25519 { 38 - t.Fatalf("identity_pub_ed25519 changed") 39 - } 40 - }
-27
maep/store.go
··· 1 - package maep 2 - 3 - import ( 4 - "context" 5 - "time" 6 - ) 7 - 8 - type Store interface { 9 - Ensure(ctx context.Context) error 10 - GetIdentity(ctx context.Context) (Identity, bool, error) 11 - PutIdentity(ctx context.Context, identity Identity) error 12 - GetContactByPeerID(ctx context.Context, peerID string) (Contact, bool, error) 13 - GetContactByNodeUUID(ctx context.Context, nodeUUID string) (Contact, bool, error) 14 - PutContact(ctx context.Context, contact Contact) error 15 - ListContacts(ctx context.Context) ([]Contact, error) 16 - AppendAuditEvent(ctx context.Context, event AuditEvent) error 17 - ListAuditEvents(ctx context.Context, peerID string, action string, limit int) ([]AuditEvent, error) 18 - AppendInboxMessage(ctx context.Context, message InboxMessage) error 19 - ListInboxMessages(ctx context.Context, fromPeerID string, topic string, limit int) ([]InboxMessage, error) 20 - AppendOutboxMessage(ctx context.Context, message OutboxMessage) error 21 - ListOutboxMessages(ctx context.Context, toPeerID string, topic string, limit int) ([]OutboxMessage, error) 22 - GetDedupeRecord(ctx context.Context, fromPeerID string, topic string, idempotencyKey string) (DedupeRecord, bool, error) 23 - PutDedupeRecord(ctx context.Context, record DedupeRecord) error 24 - PruneDedupeRecords(ctx context.Context, now time.Time, maxEntries int) (int, error) 25 - GetProtocolHistory(ctx context.Context, peerID string) (ProtocolHistory, bool, error) 26 - PutProtocolHistory(ctx context.Context, history ProtocolHistory) error 27 - }
-24
maep/topic.go
··· 1 - package maep 2 - 3 - import "strings" 4 - 5 - func IsDialogueTopic(topic string) bool { 6 - switch strings.ToLower(strings.TrimSpace(topic)) { 7 - case "share.proactive.v1", "dm.checkin.v1", "dm.reply.v1", "chat.message": 8 - return true 9 - default: 10 - return false 11 - } 12 - } 13 - 14 - func SessionScopeByTopic(topic string) string { 15 - t := strings.ToLower(strings.TrimSpace(topic)) 16 - switch { 17 - case IsDialogueTopic(t): 18 - return "dialogue.v1" 19 - case t == "": 20 - return "default" 21 - default: 22 - return "topic:" + t 23 - } 24 - }
-190
maep/types.go
··· 1 - package maep 2 - 3 - import ( 4 - "encoding/json" 5 - "time" 6 - ) 7 - 8 - const ( 9 - NodeIDPrefix = "maep:" 10 - ContactCardVersionV1 = 1 11 - ProtocolVersionV1 = 1 12 - ProtocolHelloIDV1 = "/maep/hello/1.0.0" 13 - ProtocolRPCIDV1 = "/maep/rpc/1.0.0" 14 - CapabilityDataPushV1 = "rpc.data.push.v1" 15 - JSONRPCVersion = "2.0" 16 - DefaultHelloTimeout = 3 * time.Second 17 - DefaultRPCTimeout = 10 * time.Second 18 - DefaultDialAddrTimeout = 3 * time.Second 19 - DefaultDedupeTTL = 7 * 24 * time.Hour 20 - DefaultDedupeMaxEntries = 10000 21 - DefaultDataPushRateLimit = 120 22 - MaxRPCRequestBytesV1 = 256 * 1024 23 - MaxPayloadBytesV1 = 128 * 1024 24 - ContactCardSigAlgEd25519 = "ed25519" 25 - ContactCardSigFormatJCS = "jcs-rfc8785-detached" 26 - ContactCardSignDomainV1 = "maep-contact-card-v1\n" 27 - ) 28 - 29 - type TrustState string 30 - 31 - const ( 32 - TrustStateTOFU TrustState = "tofu" 33 - TrustStateVerified TrustState = "verified" 34 - TrustStateConflicted TrustState = "conflicted" 35 - TrustStateRevoked TrustState = "revoked" 36 - ) 37 - 38 - type Identity struct { 39 - NodeUUID string `json:"node_uuid"` 40 - PeerID string `json:"peer_id"` 41 - NodeID string `json:"node_id"` 42 - IdentityPubEd25519 string `json:"identity_pub_ed25519"` 43 - IdentityPrivEd25519 string `json:"identity_priv_ed25519"` 44 - CreatedAt time.Time `json:"created_at"` 45 - UpdatedAt time.Time `json:"updated_at"` 46 - } 47 - 48 - type ContactCardPayload struct { 49 - Version int `json:"version"` 50 - NodeUUID string `json:"node_uuid"` 51 - PeerID string `json:"peer_id"` 52 - NodeID string `json:"node_id,omitempty"` 53 - IdentityPubEd25519 string `json:"identity_pub_ed25519"` 54 - Addresses []string `json:"addresses"` 55 - MinSupportedProtocol int `json:"min_supported_protocol"` 56 - MaxSupportedProtocol int `json:"max_supported_protocol"` 57 - IssuedAt time.Time `json:"issued_at"` 58 - ExpiresAt *time.Time `json:"expires_at,omitempty"` 59 - KeyRotationOf string `json:"key_rotation_of,omitempty"` 60 - } 61 - 62 - type ContactCardEnvelope struct { 63 - Payload json.RawMessage `json:"payload"` 64 - SigAlg string `json:"sig_alg"` 65 - SigFormat string `json:"sig_format"` 66 - Sig string `json:"sig"` 67 - } 68 - 69 - type ContactCard struct { 70 - Payload ContactCardPayload `json:"payload"` 71 - SigAlg string `json:"sig_alg"` 72 - SigFormat string `json:"sig_format"` 73 - Sig string `json:"sig"` 74 - } 75 - 76 - type ParsedContactCard struct { 77 - Card ContactCard 78 - CanonicalPayload []byte 79 - } 80 - 81 - type Contact struct { 82 - NodeUUID string `json:"node_uuid"` 83 - PeerID string `json:"peer_id"` 84 - NodeID string `json:"node_id"` 85 - DisplayName string `json:"display_name,omitempty"` 86 - IdentityPubEd25519 string `json:"identity_pub_ed25519"` 87 - Addresses []string `json:"addresses"` 88 - MinSupportedProtocol int `json:"min_supported_protocol"` 89 - MaxSupportedProtocol int `json:"max_supported_protocol"` 90 - IssuedAt time.Time `json:"issued_at"` 91 - ExpiresAt *time.Time `json:"expires_at,omitempty"` 92 - KeyRotationOf string `json:"key_rotation_of,omitempty"` 93 - CardSigAlg string `json:"card_sig_alg"` 94 - CardSigFormat string `json:"card_sig_format"` 95 - CardSig string `json:"card_sig"` 96 - TrustState TrustState `json:"trust_state"` 97 - LastSeen *time.Time `json:"last_seen,omitempty"` 98 - CreatedAt time.Time `json:"created_at"` 99 - UpdatedAt time.Time `json:"updated_at"` 100 - } 101 - 102 - type ImportContactResult struct { 103 - Contact Contact 104 - Created bool 105 - Updated bool 106 - Conflicted bool 107 - } 108 - 109 - type DedupeRecord struct { 110 - FromPeerID string `json:"from_peer_id"` 111 - Topic string `json:"topic"` 112 - IdempotencyKey string `json:"idempotency_key"` 113 - CreatedAt time.Time `json:"created_at"` 114 - ExpiresAt time.Time `json:"expires_at"` 115 - } 116 - 117 - type ProtocolHistory struct { 118 - PeerID string `json:"peer_id"` 119 - LastRemoteMaxProtocol int `json:"last_remote_max_protocol"` 120 - LastNegotiatedProtocol int `json:"last_negotiated_protocol"` 121 - UpdatedAt time.Time `json:"updated_at"` 122 - } 123 - 124 - type DataPushEvent struct { 125 - FromPeerID string `json:"from_peer_id"` 126 - Topic string `json:"topic"` 127 - ContentType string `json:"content_type"` 128 - PayloadBase64 string `json:"payload_base64"` 129 - PayloadBytes []byte `json:"payload_bytes,omitempty"` 130 - IdempotencyKey string `json:"idempotency_key"` 131 - SessionID string `json:"session_id,omitempty"` 132 - ReplyTo string `json:"reply_to,omitempty"` 133 - ReceivedAt time.Time `json:"received_at"` 134 - Deduped bool `json:"deduped"` 135 - } 136 - 137 - type DataPushRequest struct { 138 - Topic string `json:"topic"` 139 - ContentType string `json:"content_type"` 140 - PayloadBase64 string `json:"payload_base64"` 141 - IdempotencyKey string `json:"idempotency_key"` 142 - } 143 - 144 - type DataPushResult struct { 145 - Accepted bool `json:"accepted"` 146 - Deduped bool `json:"deduped"` 147 - } 148 - 149 - type InboxMessage struct { 150 - MessageID string `json:"message_id"` 151 - FromPeerID string `json:"from_peer_id"` 152 - Topic string `json:"topic"` 153 - ContentType string `json:"content_type"` 154 - PayloadBase64 string `json:"payload_base64"` 155 - IdempotencyKey string `json:"idempotency_key"` 156 - SessionID string `json:"session_id,omitempty"` 157 - ReplyTo string `json:"reply_to,omitempty"` 158 - ReceivedAt time.Time `json:"received_at"` 159 - } 160 - 161 - type OutboxMessage struct { 162 - MessageID string `json:"message_id"` 163 - ToPeerID string `json:"to_peer_id"` 164 - Topic string `json:"topic"` 165 - ContentType string `json:"content_type"` 166 - PayloadBase64 string `json:"payload_base64"` 167 - IdempotencyKey string `json:"idempotency_key"` 168 - SessionID string `json:"session_id,omitempty"` 169 - ReplyTo string `json:"reply_to,omitempty"` 170 - SentAt time.Time `json:"sent_at"` 171 - } 172 - 173 - const ( 174 - AuditActionContactImportCreated = "contact.import.created" 175 - AuditActionContactImportUpdated = "contact.import.updated" 176 - AuditActionContactImportConflict = "contact.import.conflict" 177 - AuditActionTrustStateChanged = "contact.trust_state.changed" 178 - ) 179 - 180 - type AuditEvent struct { 181 - EventID string `json:"event_id"` 182 - Action string `json:"action"` 183 - PeerID string `json:"peer_id,omitempty"` 184 - NodeUUID string `json:"node_uuid,omitempty"` 185 - PreviousTrustState TrustState `json:"previous_trust_state,omitempty"` 186 - NewTrustState TrustState `json:"new_trust_state,omitempty"` 187 - Reason string `json:"reason,omitempty"` 188 - Metadata map[string]string `json:"metadata,omitempty"` 189 - CreatedAt time.Time `json:"created_at"` 190 - }
+2 -4
tools/builtin/contacts_send.go
··· 21 21 type ContactsSendToolOptions struct { 22 22 Enabled bool 23 23 ContactsDir string 24 - MAEPDir string 25 24 TelegramBotToken string 26 25 TelegramBaseURL string 27 26 SlackBotToken string ··· 40 39 func (t *ContactsSendTool) Name() string { return "contacts_send" } 41 40 42 41 func (t *ContactsSendTool) Description() string { 43 - return "Sends one message to a contact. Routing is automatic across Slack, Telegram, and MAEP based on chat_id/contact reachability." 42 + return "Sends one message to a contact. Routing is automatic across Slack and Telegram based on chat_id/contact reachability." 44 43 } 45 44 46 45 func (t *ContactsSendTool) ParameterSchema() string { ··· 49 48 "properties": map[string]any{ 50 49 "contact_id": map[string]any{ 51 50 "type": "string", 52 - "description": "Target contact_id. e.g.: slack:<team_id>:<user_id>, tg:@<username>, tg:<chat_id>, maep:<peer_id>.", 51 + "description": "Target contact_id. e.g.: slack:<team_id>:<user_id>, tg:@<username>, tg:<chat_id>.", 53 52 }, 54 53 "chat_id": map[string]any{ 55 54 "type": "string", ··· 106 105 } 107 106 108 107 sender, err := contactsruntime.NewRoutingSender(ctx, contactsruntime.SenderOptions{ 109 - MAEPDir: strings.TrimSpace(t.opts.MAEPDir), 110 108 TelegramBotToken: strings.TrimSpace(t.opts.TelegramBotToken), 111 109 TelegramBaseURL: strings.TrimSpace(t.opts.TelegramBaseURL), 112 110 SlackBotToken: strings.TrimSpace(t.opts.SlackBotToken),