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
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:
1 parent
0ca715ba64
commit
b883315914
36 files changed
+1119
-563
No files matched your search
+37
-24
@@ -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: ¬es}); 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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user