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
This commit is contained in:
Ma
2026-03-28 20:32:44 +08:00
parent eb29720020
commit c95150d45f

View File

@@ -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);