Skip to content

Commit 3c6b46c

Browse files
committed
fix(agents): guard delivery-evidence attachment recursion against cycles
1 parent 1cd6f81 commit 3c6b46c

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from "vitest";
2+
import { collectDeliveredMediaUrls } from "./delivery-evidence.js";
3+
4+
describe("collectDeliveredMediaUrls attachment recursion", () => {
5+
it("collects media URLs across nested attachments", () => {
6+
const urls = collectDeliveredMediaUrls({
7+
payloads: [
8+
{
9+
url: "https://example.com/root.png",
10+
attachments: [
11+
{ mediaUrl: "https://example.com/child.png" },
12+
{ attachments: [{ filePath: "/tmp/grandchild.jpg" }] },
13+
],
14+
},
15+
],
16+
});
17+
expect(urls.toSorted()).toEqual([
18+
"/tmp/grandchild.jpg",
19+
"https://example.com/child.png",
20+
"https://example.com/root.png",
21+
]);
22+
});
23+
24+
it("does not overflow the stack on a self-referential attachments cycle", () => {
25+
// Payloads arrive as in-process `unknown` objects; a malformed self-referential
26+
// attachments chain previously recursed until the stack overflowed.
27+
const cyclic: Record<string, unknown> = { url: "https://example.com/loop.png" };
28+
cyclic.attachments = [cyclic];
29+
30+
let urls: string[] = [];
31+
expect(() => {
32+
urls = collectDeliveredMediaUrls({ payloads: [cyclic] });
33+
}).not.toThrow();
34+
expect(urls).toEqual(["https://example.com/loop.png"]);
35+
});
36+
37+
it("does not overflow on a mutual attachments cycle", () => {
38+
const a: Record<string, unknown> = { mediaUrl: "https://example.com/a.png" };
39+
const b: Record<string, unknown> = { mediaUrl: "https://example.com/b.png" };
40+
a.attachments = [b];
41+
b.attachments = [a];
42+
43+
const urls = collectDeliveredMediaUrls({ payloads: [a] });
44+
expect(urls.toSorted()).toEqual(["https://example.com/a.png", "https://example.com/b.png"]);
45+
});
46+
});

src/agents/embedded-agent-runner/delivery-evidence.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,19 @@ function collectStringValues(value: unknown, output: Set<string>) {
8080
}
8181
}
8282

83-
function collectMediaUrlsFromRecord(record: Record<string, unknown>, output: Set<string>) {
83+
function collectMediaUrlsFromRecord(
84+
record: Record<string, unknown>,
85+
output: Set<string>,
86+
// Payloads arrive as in-process `unknown` objects, so a malformed
87+
// self-referential `attachments` chain would recurse until the stack
88+
// overflows. Track visited records to bound the descent, matching
89+
// redactStringsDeep in embedded-agent-subscribe.tools.ts.
90+
seen = new WeakSet<object>(),
91+
) {
92+
if (seen.has(record)) {
93+
return;
94+
}
95+
seen.add(record);
8496
collectStringValues(record.mediaUrl, output);
8597
collectStringValues(record.mediaUrls, output);
8698
collectStringValues(record.path, output);
@@ -90,7 +102,7 @@ function collectMediaUrlsFromRecord(record: Record<string, unknown>, output: Set
90102
if (Array.isArray(attachments)) {
91103
for (const attachment of attachments) {
92104
if (attachment && typeof attachment === "object" && !Array.isArray(attachment)) {
93-
collectMediaUrlsFromRecord(attachment as Record<string, unknown>, output);
105+
collectMediaUrlsFromRecord(attachment as Record<string, unknown>, output, seen);
94106
}
95107
}
96108
}

0 commit comments

Comments
 (0)