Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183package webui
import ( "bytes" "context" "crypto/rand" "crypto/subtle" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "sync" "time")
// A page opened in a tab of its own has no window to post its messages, so// albedo web posts them for it, as the window beside the chat would// (robot-docs/artifacts.md). It has no window to hold messages either, so// past the loop guard they are refused rather than held.
const ( messageLimit = 64 << 10 // bytes of one message request unpromptedPerMinute = 5 // messages a minute without a click or key press behind each messagesPerMinute = 30 // messages a minute in all, for a page that claims every one)
// guard counts each artifact's messages over the last minute.type guard struct { mu sync.Mutex sent map[string][]sent}
type sent struct { at time.Time gesture bool}
// admit records a message unless the artifact is over either limit, and// says why when it is.func (g *guard) admit(artifact string, gesture bool, now time.Time) string { g.mu.Lock() defer g.mu.Unlock() if g.sent == nil { g.sent = map[string][]sent{} } var recent []sent unprompted := 0 for _, one := range g.sent[artifact] { if now.Sub(one.at) < time.Minute { recent = append(recent, one) if !one.gesture { unprompted++ } } } switch { case len(recent) >= messagesPerMinute: return "held: this page sent too many messages this minute" case !gesture && unprompted >= unpromptedPerMinute: return "held: this page sent several messages by itself; open it beside the chat to send more" } g.sent[artifact] = append(recent, sent{now, gesture}) return ""}
// message posts a page's message to the chat its artifact came from:// POST /frame/{id}.{key}/{version}/.albedo/message with {text, gesture}.func (s *Server) message(w http.ResponseWriter, r *http.Request) { parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/frame/"), "/") if len(parts) != 4 { writeError(w, http.StatusNotFound, "not_found", "no such artifact") return } id, key, found := strings.Cut(parts[0], ".") version, err := strconv.Atoi(parts[1]) if !found || err != nil || version < 1 { writeError(w, http.StatusNotFound, "not_found", "no such artifact") return } var body struct { Text string `json:"text"` Gesture bool `json:"gesture"` } if err := json.NewDecoder(io.LimitReader(r.Body, messageLimit)).Decode(&body); err != nil || strings.TrimSpace(body.Text) == "" { writeError(w, http.StatusBadRequest, "invalid", "a message is JSON with text") return } var artifact struct { Key string `json:"key"` Title string `json:"title"` SessionID string `json:"session_id"` SessionDeleted bool `json:"session_deleted"` } status, err := s.call(r.Context(), http.MethodGet, "/extensions/artifacts/items/"+url.PathEscape(id), nil, &artifact) if err != nil { s.refresh() writeError(w, http.StatusBadGateway, "daemon_unavailable", "the daemon did not answer: "+err.Error()) return } if status != http.StatusOK || subtle.ConstantTimeCompare([]byte(key), []byte(artifact.Key)) != 1 { writeError(w, http.StatusNotFound, "not_found", "no such artifact") return } if artifact.SessionDeleted { writeError(w, http.StatusGone, "session_deleted", "the chat this artifact came from is gone") return } if reason := s.messages.admit(id, body.Gesture, time.Now()); reason != "" { writeError(w, http.StatusTooManyRequests, "held", reason) return } input, err := inputID() if err != nil { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } text := fmt.Sprintf("From the artifact “%s” (v%d):\n\n%s", artifact.Title, version, body.Text) path := "/sessions/" + url.PathEscape(artifact.SessionID) + "/inputs/" + input status, err = s.call(r.Context(), http.MethodPut, path, map[string]string{"kind": "message", "text": text}, nil) switch { case err != nil: writeError(w, http.StatusBadGateway, "daemon_unavailable", "the daemon did not answer: "+err.Error()) case status != http.StatusAccepted: writeError(w, http.StatusBadGateway, "not_sent", fmt.Sprintf("the daemon refused the message (%d)", status)) default: w.WriteHeader(http.StatusNoContent) }}
// call sends one JSON request to the daemon with its token, decoding a// successful answer into result when there is one.func (s *Server) call(ctx context.Context, method, path string, body, result any) (int, error) { endpoint := s.upstream.Snapshot() var payload io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return 0, err } payload = bytes.NewReader(data) } request, err := http.NewRequestWithContext(ctx, method, Loopback(endpoint.Port).String()+path, payload) if err != nil { return 0, err } request.Header.Set("Authorization", "Bearer "+endpoint.Token) request.Header.Set("Accept", "application/json") if body != nil { request.Header.Set("Content-Type", "application/json") } response, err := http.DefaultClient.Do(request) if err != nil { return 0, err } defer response.Body.Close() if result != nil && response.StatusCode < 300 { if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(result); err != nil { return 0, err } } return response.StatusCode, nil}
// inputID is a fresh UUIDv7, which the daemon's input route takes as the// message's identity.func inputID() (string, error) { id := make([]byte, 16) if _, err := rand.Read(id); err != nil { return "", err } stamp := uint64(time.Now().UnixMilli()) for i := 5; i >= 0; i-- { id[i] = byte(stamp) stamp >>= 8 } id[6] = id[6]&0x0f | 0x70 id[8] = id[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", id[:4], id[4:6], id[6:8], id[8:10], id[10:]), nil}