Skip to content

Commit e727a8b

Browse files
kitepon-rgbclaude
andcommitted
fix(intercept): structuredContent.result も rewrite 対象に追加
fastmcp 系上流は tools/call レスポンスで content[] と並行して structuredContent.result[] にも同じデータを mirror する。 既存 intercept は content[] だけ走査していたので、 SC 側に container 内絶対パス (/var/lib/openai-image-tmp/...) が漏れ出していた。 大半のクライアントは content[] を消費するため実害は限定的だが、 SC を直接読む厳格クライアントだと ファイルにアクセスできず即死する。 - ToolCallEnvelope に structuredContent?.result?: ContentItem[] を追加。 - ContentItem を共通型に抽出。 - rewriteSseEventBlock に processArray() inner helper を切って content[] と structuredContent.result[] の両方を同じ規則で処理。 SHA256 prefix12 は idempotent なので 1 ファイルに対する 2 path 検出でも artifact は 1 件。 - e2e 検証: 修正後の generate_image 呼び出しで両フィールドの text が /files/<id>.jpg に書き換わり、 grep -c /var/lib/openai-image-tmp = 0 を確認。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 28f9047 commit e727a8b

1 file changed

Lines changed: 33 additions & 10 deletions

File tree

server/image-hub-app/src/intercept.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,26 @@ function reEncodeSseEvent(ev: SseEvent): string {
9595
return lines.join('\n') + '\n\n';
9696
}
9797

98+
interface ContentItem {
99+
type: string;
100+
text?: string;
101+
data?: string;
102+
mimeType?: string;
103+
[k: string]: unknown;
104+
}
105+
98106
interface ToolCallEnvelope {
99107
jsonrpc?: string;
100108
id?: number | string;
101109
result?: {
102-
content?: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
110+
content?: ContentItem[];
111+
// 一部の上流 (fastmcp 等) は content[] と並べて structuredContent.result[] にも
112+
// 同じデータを mirror して返す。 content[] だけ rewrite すると SC 側に container
113+
// 内絶対パスが漏れたままになるので、 両方を同じ規則で処理する。
114+
structuredContent?: {
115+
result?: ContentItem[];
116+
[k: string]: unknown;
117+
};
103118
[k: string]: unknown;
104119
};
105120
error?: unknown;
@@ -151,21 +166,29 @@ function rewriteSseEventBlock(block: string, rule: RewriteRule, deps: InterceptD
151166
} catch {
152167
return { out: reEncodeSseEvent(ev), rewrites: 0 };
153168
}
154-
const content = parsed.result?.content;
155-
if (!Array.isArray(content)) {
169+
const result = parsed.result;
170+
if (result === undefined || result === null) {
156171
return { out: reEncodeSseEvent(ev), rewrites: 0 };
157172
}
158173
let total = 0;
159174
let touched = false;
160-
for (const item of content) {
161-
if (item.type === 'text' && typeof item.text === 'string') {
162-
const r = rewriteText(item.text, rule, deps);
163-
if (r.rewrites > 0) {
164-
item.text = r.text;
165-
touched = true;
166-
total += r.rewrites;
175+
const processArray = (arr: ContentItem[]): void => {
176+
for (const item of arr) {
177+
if (item.type === 'text' && typeof item.text === 'string') {
178+
const r = rewriteText(item.text, rule, deps);
179+
if (r.rewrites > 0) {
180+
item.text = r.text;
181+
touched = true;
182+
total += r.rewrites;
183+
}
167184
}
168185
}
186+
};
187+
if (Array.isArray(result.content)) {
188+
processArray(result.content);
189+
}
190+
if (Array.isArray(result.structuredContent?.result)) {
191+
processArray(result.structuredContent.result);
169192
}
170193
return {
171194
out: touched ? reEncodeSseEvent({ ...ev, data: JSON.stringify(parsed) }) : reEncodeSseEvent(ev),

0 commit comments

Comments
 (0)