mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
fix: parse TaskList output without ambiguous regex (#1006)
## Summary - Replace the ambiguous regex used to parse textual `TaskList` output with linear string parsing. - Preserve support for owners, multiple dependencies, and malformed suffixes treated as subject text. - Add a regression test covering adversarial input. This addresses a potential ReDoS reported by CodeQL without changing the structured JSON path. Closes #1005. ## Testing - `npm run test:run` - `npm run build` - `npm run check`
This commit is contained in:
@@ -2092,17 +2092,30 @@ describe("applyTaskCreate / applyTaskUpdate", () => {
|
||||
it("parses the human-readable TaskList format used in session history", () => {
|
||||
expect(
|
||||
parseTaskListOutput(
|
||||
"#1 [in_progress] Run tests\n#2 [pending] Write release notes [blocked by #1]",
|
||||
"#1 [in_progress] Run tests (runner)\n#2 [pending] Write release notes [blocked by #1, #3]",
|
||||
),
|
||||
).toEqual({
|
||||
tasks: [
|
||||
{ id: "1", subject: "Run tests", status: "in_progress", blockedBy: [] },
|
||||
{ id: "2", subject: "Write release notes", status: "pending", blockedBy: ["1"] },
|
||||
{ id: "1", subject: "Run tests", status: "in_progress", owner: "runner", blockedBy: [] },
|
||||
{
|
||||
id: "2",
|
||||
subject: "Write release notes",
|
||||
status: "pending",
|
||||
blockedBy: ["1", "3"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(parseTaskListOutput("No tasks found")).toEqual({ tasks: [] });
|
||||
});
|
||||
|
||||
it("handles adversarial TaskList output in linear time", () => {
|
||||
const subject = `${"work [blocked by #1, ".repeat(20000)}work`;
|
||||
|
||||
expect(parseTaskListOutput(`#1 [pending] ${subject}`)).toEqual({
|
||||
tasks: [{ id: "1", subject, status: "pending", blockedBy: [] }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toAcpNotifications - Task* tools", () => {
|
||||
|
||||
43
src/tools.ts
43
src/tools.ts
@@ -1245,20 +1245,49 @@ export function parseTaskListOutput(content: unknown): TaskListOutput | undefine
|
||||
const tasks: TaskListOutput["tasks"] = [];
|
||||
const lines = text.trim().split("\n");
|
||||
for (const line of lines) {
|
||||
const match =
|
||||
/^#(\S+) \[(pending|in_progress|completed)\] (.+?)(?: \(([^()]*)\))?(?: \[blocked by ((?:#[^,\]]+(?:, )?)+)\])?$/.exec(
|
||||
line,
|
||||
);
|
||||
const match = /^#(\S+) \[(pending|in_progress|completed)\] (.+)$/.exec(line);
|
||||
if (!match) {
|
||||
tasks.length = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
let subject = match[3];
|
||||
let owner: string | undefined;
|
||||
let blockedBy: string[] = [];
|
||||
|
||||
const blockedMarker = " [blocked by ";
|
||||
const blockedStart = subject.lastIndexOf(blockedMarker);
|
||||
if (blockedStart > 0 && subject.endsWith("]")) {
|
||||
const dependencies = subject.slice(blockedStart + blockedMarker.length, -1).split(", ");
|
||||
if (
|
||||
dependencies.every(
|
||||
(dependency) =>
|
||||
dependency.length > 1 &&
|
||||
dependency.startsWith("#") &&
|
||||
!dependency.includes(",") &&
|
||||
!dependency.includes("]"),
|
||||
)
|
||||
) {
|
||||
subject = subject.slice(0, blockedStart);
|
||||
blockedBy = dependencies.map((dependency) => dependency.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
const ownerStart = subject.lastIndexOf(" (");
|
||||
if (ownerStart > 0 && subject.endsWith(")")) {
|
||||
const candidate = subject.slice(ownerStart + 2, -1);
|
||||
if (!candidate.includes("(") && !candidate.includes(")")) {
|
||||
subject = subject.slice(0, ownerStart);
|
||||
owner = candidate || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
tasks.push({
|
||||
id: match[1],
|
||||
subject: match[3],
|
||||
subject,
|
||||
status: match[2] as TaskListOutput["tasks"][number]["status"],
|
||||
...(match[4] ? { owner: match[4] } : {}),
|
||||
blockedBy: match[5] ? match[5].split(", ").map((id) => id.slice(1)) : [],
|
||||
...(owner ? { owner } : {}),
|
||||
blockedBy,
|
||||
});
|
||||
}
|
||||
if (tasks.length > 0) return { tasks };
|
||||
|
||||
Reference in New Issue
Block a user