package main import ( "encoding/json" "fmt" "log" "net/http" "strings" "sync" "time" "github.com/gorilla/websocket" ) const ( listenAddr = "100.85.192.60:1985" gatewayURL = "ws://127.0.0.1:18789/" gatewayToken = "601267edaccf8cd3d6afe222c3ce63602e210ff1ecc9a268" sessionKey = "agent:main:main" ) // --------------------------------------------------------------------------- // Gateway connection // --------------------------------------------------------------------------- type Gateway struct { mu sync.Mutex conn *websocket.Conn connected bool // Pending requests: reqId/runId → channel that receives gateway frames pending map[string]chan map[string]interface{} pendingMu sync.Mutex } var gw = &Gateway{pending: make(map[string]chan map[string]interface{})} func (g *Gateway) register(key string) chan map[string]interface{} { ch := make(chan map[string]interface{}, 128) g.pendingMu.Lock() g.pending[key] = ch g.pendingMu.Unlock() return ch } func (g *Gateway) unregister(key string) { g.pendingMu.Lock() delete(g.pending, key) g.pendingMu.Unlock() } func (g *Gateway) dispatch(key string, msg map[string]interface{}) bool { g.pendingMu.Lock() ch, ok := g.pending[key] g.pendingMu.Unlock() if ok { select { case ch <- msg: default: // drop if full } } return ok } func (g *Gateway) send(msg interface{}) error { g.mu.Lock() defer g.mu.Unlock() if g.conn == nil { return fmt.Errorf("not connected") } return g.conn.WriteMessage(websocket.TextMessage, mustJSON(msg)) } // connect dials the gateway, performs the challenge/connect handshake, // then enters a read loop that dispatches incoming frames. func (g *Gateway) connect() { for { conn, _, err := websocket.DefaultDialer.Dial(gatewayURL, nil) if err != nil { log.Printf("[gw] dial: %v", err) time.Sleep(3 * time.Second) continue } // 1. Read challenge var challenge map[string]interface{} if err := conn.ReadJSON(&challenge); err != nil { log.Printf("[gw] challenge read: %v", err) conn.Close() time.Sleep(3 * time.Second) continue } // 2. Send connect request connectReq := map[string]interface{}{ "type": "req", "id": fmt.Sprintf("conn-%d", time.Now().UnixMilli()), "method": "connect", "params": map[string]interface{}{ "minProtocol": 3, "maxProtocol": 3, "client": map[string]interface{}{ "id": "vault1984-chat", "version": "2026.3.2", "platform": "linux", "mode": "cli", }, "auth": map[string]string{"token": gatewayToken}, "scopes": []string{"operator.admin"}, }, } conn.WriteJSON(connectReq) // 3. Read connect response conn.SetReadDeadline(time.Now().Add(10 * time.Second)) var resp map[string]interface{} if err := conn.ReadJSON(&resp); err != nil || resp["ok"] != true { log.Printf("[gw] handshake failed: %v resp=%v", err, resp) conn.Close() time.Sleep(3 * time.Second) continue } log.Println("[gw] connected") g.mu.Lock() g.conn = conn g.connected = true g.mu.Unlock() // Keep-alive pinger go func() { for g.connected { time.Sleep(25 * time.Second) g.mu.Lock() if g.conn != nil { g.conn.WriteMessage(websocket.PingMessage, nil) } g.mu.Unlock() } }() // 4. Read loop — route frames to waiting callers for { conn.SetReadDeadline(time.Now().Add(90 * time.Second)) var frame map[string]interface{} if err := conn.ReadJSON(&frame); err != nil { log.Printf("[gw] read: %v", err) break } switch frame["type"] { case "res": // Response to a request — dispatch by id if id, _ := frame["id"].(string); id != "" { g.dispatch(id, frame) } case "event": event, _ := frame["event"].(string) if event == "chat" { if p, ok := frame["payload"].(map[string]interface{}); ok { if runId, _ := p["runId"].(string); runId != "" { g.dispatch(runId, p) } } } // Ignore health, tick, presence, etc. } } // Disconnected — reset and retry g.mu.Lock() g.conn = nil g.connected = false g.mu.Unlock() log.Println("[gw] disconnected, reconnecting...") time.Sleep(2 * time.Second) } } // Chat sends a message to the agent and collects the streamed response. func (g *Gateway) Chat(text string) (string, error) { reqId := fmt.Sprintf("req-%d", time.Now().UnixMilli()) // Register to receive the ack (keyed by reqId) ch := g.register(reqId) defer g.unregister(reqId) // Send chat.send err := g.send(map[string]interface{}{ "type": "req", "id": reqId, "method": "chat.send", "params": map[string]interface{}{ "sessionKey": sessionKey, "message": text, "idempotencyKey": reqId, }, }) if err != nil { return "", err } timeout := time.After(120 * time.Second) var buf strings.Builder runRegistered := false for { select { case <-timeout: if buf.Len() > 0 { return buf.String(), nil } return "", fmt.Errorf("timeout") case msg := <-ch: // Handle ack response to chat.send if msg["type"] == "res" { if msg["ok"] != true { errObj, _ := msg["error"].(map[string]interface{}) errMsg, _ := errObj["message"].(string) return "", fmt.Errorf("gateway: %s", errMsg) } // Extract runId and register for chat events if payload, ok := msg["payload"].(map[string]interface{}); ok { if runId, _ := payload["runId"].(string); runId != "" && !runRegistered { g.pendingMu.Lock() g.pending[runId] = ch // reuse same channel g.pendingMu.Unlock() defer g.unregister(runId) runRegistered = true } } continue } // Handle chat event (delta / final / error) state, _ := msg["state"].(string) if state == "delta" { for _, text := range extractText(msg) { buf.WriteString(text) } } if state == "final" { // If we got deltas, use them; otherwise use the final message if buf.Len() == 0 { for _, text := range extractText(msg) { buf.WriteString(text) } } result := strings.TrimSpace(buf.String()) if result == "" { return "[no response]", nil } return result, nil } if state == "error" { errMsg, _ := msg["errorMessage"].(string) if buf.Len() > 0 { return buf.String(), nil } return "", fmt.Errorf("agent error: %s", errMsg) } } } } // extractText pulls text parts from a chat event message payload. func extractText(evt map[string]interface{}) []string { msg, ok := evt["message"].(map[string]interface{}) if !ok { return nil } parts, ok := msg["content"].([]interface{}) if !ok { return nil } var texts []string for _, p := range parts { if m, ok := p.(map[string]interface{}); ok { if t, ok := m["text"].(string); ok { texts = append(texts, t) } } } return texts } // --------------------------------------------------------------------------- // Chat message store // --------------------------------------------------------------------------- type Message struct { ID int64 `json:"id"` Sender string `json:"sender"` SenderType string `json:"sender_type"` Content string `json:"content"` Timestamp int64 `json:"timestamp"` } var ( messages []Message msgMu sync.RWMutex uiClients []*websocket.Conn uiMu sync.Mutex ) func addMessage(m Message) { msgMu.Lock() messages = append(messages, m) if len(messages) > 200 { messages = messages[len(messages)-200:] } msgMu.Unlock() broadcastUI(m) } func broadcastUI(m Message) { data := mustJSON(m) uiMu.Lock() defer uiMu.Unlock() alive := make([]*websocket.Conn, 0, len(uiClients)) for _, c := range uiClients { if err := c.WriteMessage(websocket.TextMessage, data); err == nil { alive = append(alive, c) } } uiClients = alive } // --------------------------------------------------------------------------- // HTTP handlers // --------------------------------------------------------------------------- var wsUpgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} func handleWS(w http.ResponseWriter, r *http.Request) { conn, err := wsUpgrader.Upgrade(w, r, nil) if err != nil { return } // Add to broadcast list uiMu.Lock() uiClients = append(uiClients, conn) uiMu.Unlock() // Read messages from the UI for { _, data, err := conn.ReadMessage() if err != nil { break } var incoming struct { Content string `json:"content"` Sender string `json:"sender"` } json.Unmarshal(data, &incoming) if incoming.Content == "" { continue } if incoming.Sender == "" { incoming.Sender = "Johan" } userMsg := Message{ ID: time.Now().UnixMilli(), Sender: incoming.Sender, SenderType: "human", Content: incoming.Content, Timestamp: time.Now().UnixMilli(), } addMessage(userMsg) // Get agent response go func(text string) { response, err := gw.Chat(text) if err != nil { response = fmt.Sprintf("[error: %v]", err) } addMessage(Message{ ID: time.Now().UnixMilli(), Sender: "Hans", SenderType: "ai", Content: response, Timestamp: time.Now().UnixMilli(), }) }(incoming.Content) } } func handleAPI(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", 405) return } var msg Message json.NewDecoder(r.Body).Decode(&msg) if msg.Content == "" { http.Error(w, "content required", 400) return } if msg.Sender == "" { msg.Sender = "Johan" } msg.SenderType = "human" msg.Timestamp = time.Now().UnixMilli() msg.ID = msg.Timestamp addMessage(msg) go func() { response, err := gw.Chat(msg.Content) if err != nil { response = fmt.Sprintf("[error: %v]", err) } addMessage(Message{ ID: time.Now().UnixMilli(), Sender: "Hans", SenderType: "ai", Content: response, Timestamp: time.Now().UnixMilli(), }) }() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"ok": true}) } func handleMessages(w http.ResponseWriter, r *http.Request) { msgMu.RLock() defer msgMu.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(messages) } func handleStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "gateway": gw.connected, "session": sessionKey, }) } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- func main() { go gw.connect() // Wait for gateway for !gw.connected { time.Sleep(200 * time.Millisecond) } log.Printf("Vault1984 Chat ready — http://%s", listenAddr) http.HandleFunc("/", serveHTML) http.HandleFunc("/ws", handleWS) http.HandleFunc("/api/send", handleAPI) http.HandleFunc("/api/messages", handleMessages) http.HandleFunc("/api/status", handleStatus) log.Fatal(http.ListenAndServe(listenAddr, nil)) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func mustJSON(v interface{}) []byte { b, _ := json.Marshal(v) return b } // --------------------------------------------------------------------------- // Embedded UI // --------------------------------------------------------------------------- func serveHTML(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(`