A holder is now somebody who can prove it. Schema v11 stores a sha256 of a per-holder token beside the directory row, so a copied database cannot replay it and the plaintext is shown exactly once. The operator token mints and revokes those tokens; a holder token reads that holder's own memories and nothing else; naming somebody else without the operator token is refused with both ids in the message. The visibility rule was leaking before this: browse had none at all, and the search rules short-circuited to the whole library whenever no holder was named. The rule is now written out in five copies with one parameter carried four times, and a test compares the copies so they cannot drift — semgrep rejects an interpolated constant, so the copies are not a choice. Eyes carry three states on purpose. NULL means the whole library, which is what a process that owns the store is entitled to; a holder id means that holder; the empty string means nobody, which is what a tokenless HTTP caller is. Making NULL mean nobody instead would have forced every internal caller — CLI, proofs, the compiler, the bench harnesses — to name eyes, contradicting the rule that a local process reads as its own trusted principal. The capability card answers "can this store answer my kind of question" before anything is sent. It is derived from what the store holds, so it cannot promise a modality the store lacks, and it carries no memory text, so it is safe to read before trusting the store. HTTP, CLI and one MCP tool all serve it without a token.
127 lines
4.5 KiB
Go
127 lines
4.5 KiB
Go
package mcpserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/apiclient"
|
|
)
|
|
|
|
const storeCardBody = `{"schema_version":11,"atoms":3,"sources":3,"modalities":{"text":2,"image":1},
|
|
"has_audio":false,"holders":[{"holder_id":"wife","name":"小美","kind":"person","knows_about":["home"],
|
|
"contact":{"channel":"signal"},"memories":2,"modalities":{"text":2},"scenarios":["home"],
|
|
"newest_memory":"2026-05-01T12:00:00+00:00","registered":true,"token_issued":true}],"mounts":{"mounted":[],"scoped":false,"atoms_in_scope":0}}`
|
|
|
|
func cardServer(t *testing.T, seen *[]string) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
*seen = append(*seen, r.Method+" "+r.URL.Path)
|
|
if r.URL.Path == "/api/card/nobody" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"detail": "no holder 'nobody' has memories or a directory entry"})
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/card/wife" {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"holder_id":"wife","name":"小美","kind":"person","knows_about":["home"],"contact":{},"memories":2,"modalities":{"text":2},"scenarios":["home"],"newest_memory":"2026-05-01T12:00:00+00:00","registered":true,"token_issued":true}`))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(storeCardBody))
|
|
}))
|
|
}
|
|
|
|
func TestCardAsksTheStoreAndKeepsTheAnswerDerived(t *testing.T) {
|
|
var seen []string
|
|
server := cardServer(t, &seen)
|
|
defer server.Close()
|
|
|
|
s := &Server{api: apiclient.New(server.URL)}
|
|
_, out, err := s.cardTool(context.Background(), nil, CardInput{})
|
|
if err != nil {
|
|
t.Fatalf("card failed: %v", err)
|
|
}
|
|
if seen[0] != "GET /api/card" {
|
|
t.Errorf("request = %q, want the store card endpoint", seen[0])
|
|
}
|
|
if out.Card == nil || out.Holder != nil {
|
|
t.Fatalf("card = %+v holder = %+v, want the store card only", out.Card, out.Holder)
|
|
}
|
|
if out.Card.SchemaVersion != 11 || out.Card.Atoms != 3 || out.Card.HasAudio {
|
|
t.Errorf("card = %+v, want the server's own schema, counts and senses", out.Card)
|
|
}
|
|
if len(out.Card.Holders) != 1 || out.Card.Holders[0].TokenIssued != true {
|
|
t.Errorf("holders = %+v, want the directory carried through", out.Card.Holders)
|
|
}
|
|
if out.Card.Holders[0].Contact["channel"] != "signal" {
|
|
t.Errorf("contact = %+v, want the contact a referrer needs", out.Card.Holders[0].Contact)
|
|
}
|
|
}
|
|
|
|
func TestCardWithAHolderAsksForThatHolderAlone(t *testing.T) {
|
|
var seen []string
|
|
server := cardServer(t, &seen)
|
|
defer server.Close()
|
|
|
|
s := &Server{api: apiclient.New(server.URL)}
|
|
_, out, err := s.cardTool(context.Background(), nil, CardInput{HolderID: "wife"})
|
|
if err != nil {
|
|
t.Fatalf("holder card failed: %v", err)
|
|
}
|
|
if seen[0] != "GET /api/card/wife" {
|
|
t.Errorf("request = %q, want the one-holder endpoint", seen[0])
|
|
}
|
|
if out.Holder == nil || out.Card != nil {
|
|
t.Fatalf("holder = %+v card = %+v, want the holder card only", out.Holder, out.Card)
|
|
}
|
|
if out.Holder.HolderID != "wife" || out.Holder.Memories != 2 {
|
|
t.Errorf("holder card = %+v, want wife's own counts", out.Holder)
|
|
}
|
|
}
|
|
|
|
func TestCardTreatsABlankHolderAsTheWholeStore(t *testing.T) {
|
|
var seen []string
|
|
server := cardServer(t, &seen)
|
|
defer server.Close()
|
|
|
|
s := &Server{api: apiclient.New(server.URL)}
|
|
if _, _, err := s.cardTool(context.Background(), nil, CardInput{HolderID: " "}); err != nil {
|
|
t.Fatalf("blank holder failed: %v", err)
|
|
}
|
|
if seen[0] != "GET /api/card" {
|
|
t.Errorf("request = %q, want the store card rather than a path of spaces", seen[0])
|
|
}
|
|
}
|
|
|
|
func TestCardPassesOnTheServersReasonForAnUnknownHolder(t *testing.T) {
|
|
var seen []string
|
|
server := cardServer(t, &seen)
|
|
defer server.Close()
|
|
|
|
s := &Server{api: apiclient.New(server.URL)}
|
|
_, _, err := s.cardTool(context.Background(), nil, CardInput{HolderID: "nobody"})
|
|
if err == nil {
|
|
t.Fatal("an unknown holder must not answer with an empty card")
|
|
}
|
|
if !strings.Contains(err.Error(), "nobody") || !strings.Contains(err.Error(), "/api/card/nobody") {
|
|
t.Errorf("error = %q, want the endpoint and the id the server named", err)
|
|
}
|
|
}
|
|
|
|
func TestHolderCardRefusesAnEmptyIdBeforeTheNetwork(t *testing.T) {
|
|
var seen []string
|
|
server := cardServer(t, &seen)
|
|
defer server.Close()
|
|
|
|
if _, err := apiclient.New(server.URL).HolderCard(context.Background(), ""); err == nil {
|
|
t.Fatal("an empty holder id should be refused, not fetched")
|
|
}
|
|
if len(seen) != 0 {
|
|
t.Errorf("requests = %v, want none", seen)
|
|
}
|
|
}
|