feat(api): one API for curl and MCP, REST reads with keys, origin badge with key name
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m16s

API keys reach tasks and lists over plain REST too (GET /task-lists, GET /tasks with
status/due filters, PATCH/DELETE /thoughts/{id}, GET /lists, list items), guarded by a route
table; notes and settings stay closed. Rights hold for every access alike (webhook:task also
allows create_task over MCP, tasks:write also task delivery).

Tasks, thoughts and list items record which key created them and which one changed them last
(migration 13 replaces agent_mark; origin stays after the user's own edits and survives the
triage into a list item). OriginBadge shows the key name with a webhook or robot icon, details
on hover and tap, and replaces the Webhook and KI badges everywhere, now also on list items.

Settings: category "API" with rights grouped by tasks, lists and inbox and two tabs
(curl / smart home with a Home Assistant sensor, AI agents over MCP). Docs, help, README and
changelog updated; v0.30.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 10:26:44 +01:00
1 parent 0ca715ba64
commit b883315914
36 files changed
+1119 -563

No files matched your search

+37 -24
View File
@@ -197,13 +197,13 @@ func TestMCPTasksAndLists(t *testing.T) {
}
}
// Agents can create task lists and lists and delete tasks; their entries carry a mark until the user
// changes them.
func TestMCPCreateDeleteAndAgentMark(t *testing.T) {
// Agents can create task lists and lists and delete tasks; what a key creates or changes keeps its
// origin for good, also after the user's own edits.
func TestMCPCreateDeleteAndOrigin(t *testing.T) {
srv, store, token := setupTestServer(t)
router := srv.Router()
claims, _ := auth.ValidateToken(testJWTSecret, token)
key, _ := store.CreateAPIKey(claims.UserID, "Agent", []string{"tasks:read", "tasks:write", "lists:read", "lists:write"})
key, _ := store.CreateAPIKey(claims.UserID, "Claude Code", []string{"tasks:read", "tasks:write", "lists:read", "lists:write"})
call := func(id int, name, args string) map[string]interface{} {
t.Helper()
_, res := rpc(t, router, key.RawKey, fmt.Sprintf(`{"jsonrpc":"2.0","id":%d,"method":"tools/call","params":{"name":%q,"arguments":%s}}`, id, name, args))
@@ -222,25 +222,34 @@ func TestMCPCreateDeleteAndAgentMark(t *testing.T) {
task := call(2, "create_task", `{"content":"Von Claude","taskListId":"`+tlID+`"}`)
taskID := task["id"].(string)
th, _ := store.GetThoughtByID(claims.UserID, taskID)
if th.AgentMark != "created" || th.TaskListID != tlID {
t.Fatalf("agent task should be marked created: %+v", th)
}
// An agent's change keeps "created"; the user's own change removes the mark
call(3, "update_task", `{"id":"`+taskID+`","priority":"high"}`)
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.AgentMark != "created" {
t.Fatalf("mark after agent edit: %q", th.AgentMark)
if th.Origin == nil || th.Origin.CreatedVia != "mcp" || th.Origin.CreatedKey != "Claude Code" || th.TaskListID != tlID {
t.Fatalf("agent task should carry its origin: %+v", th.Origin)
}
// The user's own edit keeps the origin
notes := "selbst ergänzt"
if _, err := srv.updateTask(claims.UserID, taskID, taskPatch{Notes: &notes}); err != nil {
t.Fatal(err)
}
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.AgentMark != "" {
t.Fatalf("user edit should clear the mark, got %q", th.AgentMark)
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.Origin == nil || th.Origin.CreatedKey != "Claude Code" || th.Origin.ChangedVia != "" {
t.Fatalf("user edit must keep the origin and not count as a key change: %+v", th.Origin)
}
call(4, "complete_task", `{"id":"`+taskID+`"}`)
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.AgentMark != "edited" {
t.Fatalf("agent edit of a user task should mark edited, got %q", th.AgentMark)
// A task of the user changed by the agent: changed via MCP, with key and time
own, _ := srv.createTask(claims.UserID, "Von mir", "", "manual", "", "", "", nil)
if own.Origin != nil {
t.Fatalf("own task must not have an origin: %+v", own.Origin)
}
call(3, "complete_task", `{"id":"`+own.ID+`"}`)
if th, _ = store.GetThoughtByID(claims.UserID, own.ID); th.Origin == nil || th.Origin.CreatedVia != "" ||
th.Origin.ChangedVia != "mcp" || th.Origin.ChangedKey != "Claude Code" || th.Origin.ChangedAt == "" {
t.Fatalf("agent change of a user task: %+v", th.Origin)
}
// Renamed or deleted keys show up as such
_ = store.DeleteAPIKey(claims.UserID, key.ID)
if th, _ = store.GetThoughtByID(claims.UserID, own.ID); th.Origin.ChangedKey != "Gelöschter Schlüssel" {
t.Fatalf("deleted key name: %q", th.Origin.ChangedKey)
}
key, _ = store.CreateAPIKey(claims.UserID, "Claude Code", []string{"tasks:read", "tasks:write", "lists:read", "lists:write"})
call(5, "delete_task", `{"id":"`+taskID+`"}`)
if th, err := store.GetThoughtByID(claims.UserID, taskID); err == nil && th != nil {
@@ -253,18 +262,15 @@ func TestMCPCreateDeleteAndAgentMark(t *testing.T) {
t.Fatalf("create_list: %v", l)
}
item := call(7, "add_list_item", `{"listId":"`+listID+`","content":"Zahnbürste"}`)
if item["agentMark"] != "created" {
t.Fatalf("agent item should be marked created: %v", item)
if o, _ := item["origin"].(map[string]interface{}); o == nil || o["createdVia"] != "mcp" {
t.Fatalf("agent item should carry its origin: %v", item)
}
items, _ := store.GetListItems(listID)
if len(items) != 1 || items[0].AgentMark != "created" {
t.Fatalf("stored item mark: %+v", items)
}
if _, err := srv.setListItemDone(claims.UserID, listID, items[0].ID, true, false); err != nil {
if _, err := srv.setListItemDone(claims.UserID, listID, items[0].ID, true, nil); err != nil {
t.Fatal(err)
}
if items, _ = store.GetListItems(listID); items[0].AgentMark != "" {
t.Fatalf("ticking by the user should clear the mark: %q", items[0].AgentMark)
if items, _ = store.GetListItems(listID); items[0].Origin == nil || items[0].Origin.CreatedKey != "Claude Code" {
t.Fatalf("ticking by the user keeps the origin: %+v", items[0].Origin)
}
// Other people's tasks cannot be deleted
@@ -274,4 +280,11 @@ func TestMCPCreateDeleteAndAgentMark(t *testing.T) {
if _, isErr := callResult(t, res); !isErr {
t.Fatalf("key must not delete foreign tasks")
}
// The narrow right "create tasks" also allows create_task over MCP, but not update_task
creator, _ := store.CreateAPIKey(claims.UserID, "Nur anlegen", []string{"webhook:task"})
_, res = rpc(t, router, creator.RawKey, `{"jsonrpc":"2.0","id":9,"method":"tools/list"}`)
if names := strings.Join(toolNames(res), ","); !strings.Contains(names, "create_task") || strings.Contains(names, "update_task") {
t.Fatalf("webhook:task key tools: %s", names)
}
}