From fe1353182d7a4dcfa1ebb96625cc161568663c34 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:17:16 +0900 Subject: [PATCH] fix: bound Fernet slot runs, Kiro error-body read, and skill-path line slice --- src/adapters/kiro/adapter.ts | 1 + src/adapters/kiro/stream.ts | 4 ++- src/claude/inbound.ts | 11 +++++-- src/server/responses/encrypted-payload.ts | 4 +++ .../claude-integration/claude-inbound.test.ts | 6 ++++ .../multi-agent-compat.test.ts | 15 +++++++++ tests/providers/kiro/kiro-stream.test.ts | 32 +++++++++++++++++++ 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/adapters/kiro/adapter.ts b/src/adapters/kiro/adapter.ts index b6a374cf4cc..0300c52907f 100644 --- a/src/adapters/kiro/adapter.ts +++ b/src/adapters/kiro/adapter.ts @@ -250,6 +250,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter }); return { response, + abortSignal: requestAbortSignal, inputTokens: retry.inputTokens, contextInputEstimate: retry.contextInputEstimate, nameMap: retry.nameMap, diff --git a/src/adapters/kiro/stream.ts b/src/adapters/kiro/stream.ts index d10ab1105c0..1e89d420f7c 100644 --- a/src/adapters/kiro/stream.ts +++ b/src/adapters/kiro/stream.ts @@ -19,6 +19,7 @@ import { noteKiroTransientThrottle } from "../kiro-retry"; import { KiroThinkingParser } from "../kiro-thinking"; import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation"; import { isValidKiroConversationId } from "../kiro-wire"; +import { readDisplaySafeErrorPayloadText } from "../upstream-http-error"; import { tagKiroReasoningBlob } from "./reasoning"; import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage"; @@ -74,6 +75,7 @@ function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetent interface KiroFallbackAttempt { response: Response; + abortSignal?: AbortSignal; inputTokens: number; contextInputEstimate: number; nameMap: Map; @@ -1092,7 +1094,7 @@ export async function* parseKiroStream( firstResult.releaseRetained(); fallback.releaseRequestBody?.(); if (!fallback.response.ok) { - const payload = await fallback.response.text().catch(() => ""); + const payload = await readDisplaySafeErrorPayloadText(fallback.response, fallback.abortSignal); const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload); yield { type: "error", diff --git a/src/claude/inbound.ts b/src/claude/inbound.ts index 2938c9b2a9d..489075b1e5b 100644 --- a/src/claude/inbound.ts +++ b/src/claude/inbound.ts @@ -118,6 +118,7 @@ export function effectiveBlockedSkillNames(cc?: Pick SKILL_TEXT_PATH_MAX_CHARS) return text; + const dir = pathPrefix.slice(0, firstLineEnd === -1 ? pathPrefix.length : firstLineEnd).trim(); // Windows clients send `C:\Users\...\claude-api`; normalize separators before // basenaming (repo precedent: src/codex/inject.ts isOpencodexCatalogPath). - const base = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop()?.toLowerCase() ?? ""; + const normalizedDir = dir.replace(/\\/g, "/").replace(/\/+$/, ""); + const base = normalizedDir.slice(normalizedDir.lastIndexOf("/") + 1).toLowerCase(); if (!names.includes(base)) return text; return `[opencodex] '${base}' skill document bundle (${text.length} chars) elided for routed models ` + "(claudeCode.blockedSkills). The skill is loaded; answer from general knowledge instead of citing the bundle."; diff --git a/src/server/responses/encrypted-payload.ts b/src/server/responses/encrypted-payload.ts index 9835e851cca..9e5e71ee46b 100644 --- a/src/server/responses/encrypted-payload.ts +++ b/src/server/responses/encrypted-payload.ts @@ -123,6 +123,9 @@ function looksLikeUnknownOpaqueSlot(payload: string): boolean { */ const FERNET_TOKEN_CANDIDATE = /g[A-Za-z0-9_-]{97,}={0,2}/g; const FERNET_TOKEN_BOUNDARY_CHAR = /[A-Za-z0-9_=-]/; +// A normal mixed agent task contains one encrypted body. Keep pathological slots +// from amplifying into an attacker-controlled number of request parts. +const MAX_EMBEDDED_FERNET_RUNS_PER_SLOT = 64; interface FernetTokenRun { index: number; @@ -173,6 +176,7 @@ function fernetTokenRuns(payload: string): FernetTokenRun[] { if (after && FERNET_TOKEN_BOUNDARY_CHAR.test(after)) continue; if (!isStructurallyValidFernetToken(token)) continue; runs.push({ index, token }); + if (runs.length >= MAX_EMBEDDED_FERNET_RUNS_PER_SLOT) break; } return runs; } diff --git a/tests/claude-integration/claude-inbound.test.ts b/tests/claude-integration/claude-inbound.test.ts index 5631ec1271a..308f06b0c80 100644 --- a/tests/claude-integration/claude-inbound.test.ts +++ b/tests/claude-integration/claude-inbound.test.ts @@ -675,6 +675,12 @@ describe("bundled-skill elision for routed models (devlog 260712 060)", () => { const texts = userTexts(requestWithSkillTextBlock("claude-api", 500_000, undefined, "C:claude-api")); expect(texts.some(t => t.length > 400_000)).toBe(true); }); + + test("text-block carrier: oversized marker paths pass through without unbounded parsing", () => { + const oversizedDir = `/${"/".repeat(10_000)}claude-api`; + const texts = userTexts(requestWithSkillTextBlock("claude-api", 20_000, undefined, oversizedDir)); + expect(texts.some(t => t.startsWith(`Base directory for this skill: ${oversizedDir}`))).toBe(true); + }); }); describe("ocx-route directive (devlog 072)", () => { diff --git a/tests/codex-integration/multi-agent-compat.test.ts b/tests/codex-integration/multi-agent-compat.test.ts index d4c96d8513e..1519c53a05b 100644 --- a/tests/codex-integration/multi-agent-compat.test.ts +++ b/tests/codex-integration/multi-agent-compat.test.ts @@ -1478,6 +1478,21 @@ describe("sanitizeEncryptedContentInPlace", () => { const parts = (input[0] as { content: Array> }).content; expect(parts[0]).toEqual({ type: "encrypted_content", encrypted_content: fernet }); }); + + test("mixed slots cap Fernet expansion", () => { + const payload = Array.from({ length: 1_000 }, () => fernetFixture()).join("."); + const input = [ + { type: "message", role: "user", content: [ + { type: "encrypted_content", encrypted_content: `preamble.${payload}` }, + ] }, + ]; + + expect(sanitizeEncryptedContentInPlace(input)).toBe(1); + const parts = (input[0] as { content: Array> }).content; + expect(parts.length).toBeLessThanOrEqual(129); + expect(parts.filter(part => part.type === "encrypted_content")).toHaveLength(64); + expect(parts.at(-1)?.type).toBe("input_text"); + }); }); describe("spawn-message delivery (agent_message + encrypted slot)", () => { diff --git a/tests/providers/kiro/kiro-stream.test.ts b/tests/providers/kiro/kiro-stream.test.ts index 336f264d021..068951100ee 100644 --- a/tests/providers/kiro/kiro-stream.test.ts +++ b/tests/providers/kiro/kiro-stream.test.ts @@ -369,6 +369,38 @@ describe("kiro adapter — parseStream", () => { }); }); + test("fallback HTTP errors stop reading oversized upstream bodies", async () => { + const chunk = new TextEncoder().encode("A".repeat(32 * 1024)); + let pulls = 0; + let cancelled = false; + globalThis.fetch = (async () => new Response(new ReadableStream({ + pull(controller) { + pulls += 1; + controller.enqueue(chunk); + }, + cancel() { + cancelled = true; + }, + }), { + status: 400, + headers: { "content-type": "text/plain" }, + })) as typeof fetch; + const adapter = createKiroAdapter(provider); + await adapter.buildRequest(parsedWith([{ role: "user", content: "do it" }], [bashTool])); + + const events = await collectAdapterEvents(adapter.parseStream(new Response(streamOf( + eventFrame({ content: "I am checking." }), + )))); + + expect(cancelled).toBe(true); + expect(pulls).toBeLessThan(10); + expect(events.at(-1)).toMatchObject({ + type: "error", + status: 400, + retryable: false, + }); + }); + test("large first-attempt text stays charged through fallback construction and releases after parse", async () => { const budget = createTranslatorBudget(); const firstText = "x".repeat(10 * 1024 * 1024);