Przeglądaj źródła

fix: add execution-layer guards to agent tool dispatch

- write_draft: verify sequential chapter order, reject gap writes
- revise_chapter: verify target chapter exists and has content
- write_truth_file: block chapter progress manipulation in current_state.md
- import_chapters: require minimum 2 chapters (whole-book reimport only)

Addresses #92 #93 execution-layer hardening
Ma 7 miesięcy temu
rodzic
commit
c95150d45f
1 zmienionych plików z 35 dodań i 3 usunięć
  1. 35 3
      packages/core/src/pipeline/agent.ts

+ 35 - 3
packages/core/src/pipeline/agent.ts

@@ -370,8 +370,15 @@ export async function executeAgentTool(
     }
 
     case "write_draft": {
+      // Guard: write_draft only writes the next sequential chapter
+      const bookId = args.bookId as string;
+      const nextNum = await state.getNextChapterNumber(bookId);
+      const index = await state.loadChapterIndex(bookId);
+      if (index.length > 0 && index[index.length - 1]!.number !== nextNum - 1) {
+        return JSON.stringify({ error: `write_draft 只能续写下一章(第${nextNum}章)。如果中间有空缺,请先用 get_book_status 确认状态。` });
+      }
       const result = await pipeline.writeDraft(
-        args.bookId as string,
+        bookId,
         args.guidance as string | undefined,
       );
       return JSON.stringify(result);
@@ -386,9 +393,22 @@ export async function executeAgentTool(
     }
 
     case "revise_chapter": {
+      // Guard: target chapter must exist and have content
+      const bookId = args.bookId as string;
+      const chapterNum = args.chapterNumber as number | undefined;
+      if (chapterNum !== undefined) {
+        const index = await state.loadChapterIndex(bookId);
+        const chapter = index.find((ch) => ch.number === chapterNum);
+        if (!chapter) {
+          return JSON.stringify({ error: `第${chapterNum}章不存在。revise_chapter 只能修订已有章节,不能用来补写缺失章节。请用 get_book_status 确认。` });
+        }
+        if (chapter.wordCount === 0) {
+          return JSON.stringify({ error: `第${chapterNum}章内容为空(0字)。revise_chapter 不能修订空章节。` });
+        }
+      }
       const result = await pipeline.reviseDraft(
-        args.bookId as string,
-        args.chapterNumber as number | undefined,
+        bookId,
+        chapterNum,
         (args.mode as ReviseMode) ?? DEFAULT_REVISE_MODE,
       );
       return JSON.stringify(result);
@@ -524,6 +544,10 @@ export async function executeAgentTool(
       if (chapters.length === 0) {
         return JSON.stringify({ error: "No chapters found. Check text format or provide a splitPattern." });
       }
+      // Guard: import_chapters is a whole-book reimport, not a single-chapter patch
+      if (chapters.length === 1) {
+        return JSON.stringify({ error: "import_chapters 是整书重导工具,需要至少 2 个章节。如果只想补一章,请用 write_draft 续写或 revise_chapter 修订。" });
+      }
       const result = await pipeline.importChapters({
         bookId: args.bookId as string,
         chapters: [...chapters],
@@ -548,6 +572,14 @@ export async function executeAgentTool(
         return JSON.stringify({ error: `不允许修改文件 "${fileName}"。允许的文件:${ALLOWED_FILES.join(", ")}` });
       }
 
+      // Guard: block chapter progress manipulation via current_state.md
+      if (fileName === "current_state.md") {
+        const progressPatterns = /(?:lastAppliedChapter|chapter.*:\s*\d+|当前章.*[::]\s*\d+|进度.*[::]\s*\d+)/i;
+        if (progressPatterns.test(content)) {
+          return JSON.stringify({ error: "不允许通过 write_truth_file 修改 current_state.md 中的章节进度。章节进度由系统自动管理。" });
+        }
+      }
+
       const { writeFile, mkdir } = await import("node:fs/promises");
       const { join } = await import("node:path");
       const bookDir = new (await import("../state/manager.js")).StateManager(config.projectRoot).bookDir(bookId);