From 5938fa6ffbc96101bf62832d97b37f73c74edb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ad=C3=A1mek?= Date: Wed, 8 Apr 2026 16:08:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20shared=20?= =?UTF-8?q?PROTECTED=5FSYSTEM=5FNOTES,=20protection=20checks,=20soft=20del?= =?UTF-8?q?ete=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move PROTECTED_SYSTEM_NOTES to helpers.ts for shared use - move_note: check against full system notes set, add protected parent check - clone_note: add source note protection + protected parent checks - delete_note: fix description to say 'soft delete' (recoverable) --- apps/server/src/services/llm/tools/helpers.ts | 3 +++ .../src/services/llm/tools/hierarchy_tools.ts | 26 ++++++++++++++----- .../src/services/llm/tools/note_tools.ts | 7 ++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/server/src/services/llm/tools/helpers.ts b/apps/server/src/services/llm/tools/helpers.ts index fbfebb1e12..7c4ef91f60 100644 --- a/apps/server/src/services/llm/tools/helpers.ts +++ b/apps/server/src/services/llm/tools/helpers.ts @@ -13,6 +13,9 @@ const ATTACHMENT_PREVIEW_MAX_LENGTH = 200; /** Skip expensive content loading/conversion for notes larger than this. */ const CONTENT_PREVIEW_SIZE_THRESHOLD = 10_000; +/** Note IDs that must not be deleted, moved, or cloned by the LLM. */ +export const PROTECTED_SYSTEM_NOTES = new Set(["root", "_hidden", "_share", "_lbRoot", "_globalNoteMap"]); + /** * Return `true` if the value is truthy, otherwise `undefined`. * Since `undefined` values are omitted from JSON serialization, diff --git a/apps/server/src/services/llm/tools/hierarchy_tools.ts b/apps/server/src/services/llm/tools/hierarchy_tools.ts index 2e39f2ed31..e111cb5687 100644 --- a/apps/server/src/services/llm/tools/hierarchy_tools.ts +++ b/apps/server/src/services/llm/tools/hierarchy_tools.ts @@ -8,6 +8,7 @@ import becca from "../../../becca/becca.js"; import type BNote from "../../../becca/entities/bnote.js"; import branchService from "../../branches.js"; import cloningService from "../../cloning.js"; +import { PROTECTED_SYSTEM_NOTES } from "./helpers.js"; import { defineTools } from "./tool_registry.js"; //#region Subtree tool implementation @@ -105,8 +106,8 @@ export const hierarchyTools = defineTools({ if (!note) { return { error: "Note not found" }; } - if (note.noteId === "root") { - return { error: "Cannot move the root note" }; + if (PROTECTED_SYSTEM_NOTES.has(noteId)) { + return { error: "Cannot move system notes" }; } if (note.isProtected) { return { error: "Note is protected and cannot be moved" }; @@ -116,6 +117,9 @@ export const hierarchyTools = defineTools({ if (!targetParent) { return { error: "Target parent note not found" }; } + if (!targetParent.isContentAvailable()) { + return { error: "Cannot move note to a protected parent" }; + } // Use the first (primary) parent branch for the move const branches = note.getParentBranches(); @@ -152,18 +156,28 @@ export const hierarchyTools = defineTools({ }), mutates: true, execute: ({ noteId, parentNoteId, prefix }) => { + const note = becca.getNote(noteId); + if (!note) { + return { error: "Note not found" }; + } + if (note.isProtected) { + return { error: "Note is protected and cannot be cloned" }; + } + + const parent = becca.getNote(parentNoteId); + if (parent && !parent.isContentAvailable()) { + return { error: "Cannot clone note to a protected parent" }; + } + const result = cloningService.cloneNoteToParentNote(noteId, parentNoteId, prefix ?? null); if (!result.success) { return { error: result.message || "Clone failed" }; } - const note = becca.getNote(noteId); - const parent = becca.getNote(parentNoteId); - return { success: true, noteId, - title: note?.getTitleOrProtected() ?? noteId, + title: note.getTitleOrProtected(), parentNoteId, parentTitle: parent?.getTitleOrProtected() ?? parentNoteId, branchId: result.branchId diff --git a/apps/server/src/services/llm/tools/note_tools.ts b/apps/server/src/services/llm/tools/note_tools.ts index e3d2d12432..8a354175f3 100644 --- a/apps/server/src/services/llm/tools/note_tools.ts +++ b/apps/server/src/services/llm/tools/note_tools.ts @@ -10,12 +10,9 @@ import noteService from "../../notes.js"; import SearchContext from "../../search/search_context.js"; import searchService from "../../search/services/search.js"; import TaskContext from "../../task_context.js"; -import { TOOL_LIMITS, getContentPreview, getNoteContentForLlm, getNoteMeta, setNoteContentFromLlm } from "./helpers.js"; +import { PROTECTED_SYSTEM_NOTES, TOOL_LIMITS, getContentPreview, getNoteContentForLlm, getNoteMeta, setNoteContentFromLlm } from "./helpers.js"; import { defineTools } from "./tool_registry.js"; -/** Note IDs that must not be deleted or moved by the LLM. */ -const PROTECTED_SYSTEM_NOTES = new Set(["root", "_hidden", "_share", "_lbRoot", "_globalNoteMap"]); - export const noteTools = defineTools({ search_notes: { description: [ @@ -270,7 +267,7 @@ export const noteTools = defineTools({ }, delete_note: { - description: "Delete a note and all its branches (parent links). This is irreversible. The note will be marked as deleted. Cannot delete system notes (root, _hidden, etc.).", + description: "Delete a note and all its branches (parent links). This is a soft delete (recoverable via 'Recent Changes'). Cannot delete system notes (root, _hidden, etc.).", inputSchema: z.object({ noteId: z.string().describe("The ID of the note to delete") }),