fix(kiro): intercept chat via x-amz-target and prepend initial-response frame
Kiro IDE 1.0.228+ moved GenerateAssistantResponse from path /generateAssistantResponse to POST / + x-amz-target header, so chat turns bypassed MITM. The SmithyMessageDecoderStream also now requires an initial-response frame at stream start, and agent/vibe mode sends modelId "auto" which had no mappable slot. - Add isChatRequest() header-based match for kiro in mitm/config.js - Add buildInitialResponseFrame/withInitialFrame to emit the mandatory initial-response once per stream (kiro.js) - Add "auto" model slot and update mitmDomain to runtime.us-east-1.kiro.dev
This commit is contained in:
parent
b57c041345
commit
5b417f9bf2
5 changed files with 67 additions and 15 deletions
|
|
@ -26,10 +26,28 @@ const TARGET_HOSTS = [
|
||||||
const URL_PATTERNS = {
|
const URL_PATTERNS = {
|
||||||
antigravity: [":generateContent", ":streamGenerateContent"],
|
antigravity: [":generateContent", ":streamGenerateContent"],
|
||||||
copilot: ["/chat/completions", "/v1/messages", "/responses"],
|
copilot: ["/chat/completions", "/v1/messages", "/responses"],
|
||||||
|
// Legacy path form. Kiro IDE 1.0.228+ posts to `/` with x-amz-target instead —
|
||||||
|
// see isChatRequest() for the header-based match.
|
||||||
kiro: ["/generateAssistantResponse"],
|
kiro: ["/generateAssistantResponse"],
|
||||||
cursor: ["/BidiAppend", "/RunSSE", "/RunPoll", "/Run"],
|
cursor: ["/BidiAppend", "/RunSSE", "/RunPoll", "/Run"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this request is a chat turn we should intercept (vs passthrough).
|
||||||
|
* Kiro Runtime moved GenerateAssistantResponse from path `/generateAssistantResponse`
|
||||||
|
* to `POST /` + `x-amz-target: KiroRuntimeService.GenerateAssistantResponse`
|
||||||
|
* (verified via live mitmproxy capture of Kiro IDE 1.0.228).
|
||||||
|
*/
|
||||||
|
function isChatRequest(tool, req) {
|
||||||
|
const patterns = URL_PATTERNS[tool] || [];
|
||||||
|
if (patterns.some((p) => (req.url || "").includes(p))) return true;
|
||||||
|
if (tool === "kiro") {
|
||||||
|
const target = String(req.headers?.["x-amz-target"] || "");
|
||||||
|
return target.includes("GenerateAssistantResponse");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Synonym map: rawModel from request → canonical alias key in mitmAlias DB
|
// Synonym map: rawModel from request → canonical alias key in mitmAlias DB
|
||||||
const MODEL_SYNONYMS = {
|
const MODEL_SYNONYMS = {
|
||||||
antigravity: {
|
antigravity: {
|
||||||
|
|
@ -127,4 +145,4 @@ function extractModel(url, body) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { IS_DEV, LSOF_BIN, TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, MODEL_NO_MAP, LOG_BLACKLIST_URL_PARTS, getToolForHost, extractModel };
|
module.exports = { IS_DEV, LSOF_BIN, TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, MODEL_NO_MAP, LOG_BLACKLIST_URL_PARTS, getToolForHost, isChatRequest, extractModel };
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ function initKiroState(modelId) {
|
||||||
finishSent: false, // Whether termination has been emitted
|
finishSent: false, // Whether termination has been emitted
|
||||||
usage: null, // Accumulated usage from usage-only chunks
|
usage: null, // Accumulated usage from usage-only chunks
|
||||||
inThink: false, // Whether inside a <thinking> block
|
inThink: false, // Whether inside a <thinking> block
|
||||||
thinkBuf: "" // Buffer for partial thinking content
|
thinkBuf: "", // Buffer for partial thinking content
|
||||||
|
initialSent: false, // Whether initial-response frame was emitted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,9 +131,9 @@ function encodeHeader(name, value) {
|
||||||
* The SmithyMessageDecoderStream layer requires three system headers on every frame:
|
* The SmithyMessageDecoderStream layer requires three system headers on every frame:
|
||||||
* :message-type = "event" (or "exception" / "error")
|
* :message-type = "event" (or "exception" / "error")
|
||||||
* :event-type = e.g. "assistantResponseEvent"
|
* :event-type = e.g. "assistantResponseEvent"
|
||||||
* :content-type = "application/json"
|
* :content-type = "application/json" (initial-response uses x-amz-json-1.0)
|
||||||
*/
|
*/
|
||||||
function buildEventStreamFrame(eventType, payload) {
|
function buildEventStreamFrame(eventType, payload, contentType = "application/json") {
|
||||||
const payloadBuf = Buffer.from(
|
const payloadBuf = Buffer.from(
|
||||||
typeof payload === "string" ? payload : JSON.stringify(payload),
|
typeof payload === "string" ? payload : JSON.stringify(payload),
|
||||||
"utf8"
|
"utf8"
|
||||||
|
|
@ -142,7 +143,7 @@ function buildEventStreamFrame(eventType, payload) {
|
||||||
const headersBuf = Buffer.concat([
|
const headersBuf = Buffer.concat([
|
||||||
encodeHeader(":message-type", "event"),
|
encodeHeader(":message-type", "event"),
|
||||||
encodeHeader(":event-type", eventType),
|
encodeHeader(":event-type", eventType),
|
||||||
encodeHeader(":content-type", "application/json"),
|
encodeHeader(":content-type", contentType),
|
||||||
]);
|
]);
|
||||||
const headersLen = headersBuf.length;
|
const headersLen = headersBuf.length;
|
||||||
|
|
||||||
|
|
@ -159,6 +160,24 @@ function buildEventStreamFrame(eventType, payload) {
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Real Kiro Runtime always starts the stream with this frame (capture of IDE 1.0.228). */
|
||||||
|
function buildInitialResponseFrame(conversationId = "") {
|
||||||
|
return buildEventStreamFrame(
|
||||||
|
"initial-response",
|
||||||
|
{ conversationId: conversationId || "" },
|
||||||
|
"application/x-amz-json-1.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prepend initial-response once per stream so Smithy decoder is happy. */
|
||||||
|
function withInitialFrame(state, frames) {
|
||||||
|
const list = frames == null ? [] : Array.isArray(frames) ? frames : [frames];
|
||||||
|
if (state.initialSent) return list.length === 0 ? null : list.length === 1 ? list[0] : list;
|
||||||
|
state.initialSent = true;
|
||||||
|
const out = [buildInitialResponseFrame(""), ...list];
|
||||||
|
return out.length === 1 ? out[0] : out;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── CodeWhisperer → OpenAI conversion ───────────────────────────────────────
|
// ─── CodeWhisperer → OpenAI conversion ───────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -321,12 +340,12 @@ function convertOpenAIToKiro(chunk, state) {
|
||||||
state.inThink = false;
|
state.inThink = false;
|
||||||
const thinking = state.thinkBuf;
|
const thinking = state.thinkBuf;
|
||||||
state.thinkBuf = "";
|
state.thinkBuf = "";
|
||||||
return buildEventStreamFrame("reasoningContentEvent", {
|
return withInitialFrame(state, buildEventStreamFrame("reasoningContentEvent", {
|
||||||
content: thinking,
|
content: thinking,
|
||||||
modelId: state.modelId || "kiro-unknown"
|
modelId: state.modelId || "kiro-unknown"
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
return buildEventStreamFrame("messageStopEvent", {});
|
return withInitialFrame(state, buildEventStreamFrame("messageStopEvent", {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const frames = [];
|
const frames = [];
|
||||||
|
|
@ -408,8 +427,12 @@ function convertOpenAIToKiro(chunk, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frames.length === 0) return null;
|
if (frames.length === 0) {
|
||||||
return frames.length === 1 ? frames[0] : frames;
|
// اولین چانک ممکنه فقط role/empty باشه — initial رو همونجا بفرست
|
||||||
|
if (!state.initialSent) return withInitialFrame(state, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return withInitialFrame(state, frames.length === 1 ? frames[0] : frames);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const dns = require("dns");
|
||||||
const { promisify } = require("util");
|
const { promisify } = require("util");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const { log, err, dumpRequest, createResponseDumper, clearDumpDir } = require("./logger");
|
const { log, err, dumpRequest, createResponseDumper, clearDumpDir } = require("./logger");
|
||||||
const { IS_DEV, LSOF_BIN, TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, MODEL_NO_MAP, getToolForHost, extractModel } = require("./config");
|
const { IS_DEV, LSOF_BIN, TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, MODEL_NO_MAP, getToolForHost, isChatRequest, extractModel } = require("./config");
|
||||||
const { DATA_DIR, MITM_DIR } = require("./paths");
|
const { DATA_DIR, MITM_DIR } = require("./paths");
|
||||||
const { generateCert, getCertForDomain } = require("./cert/generate");
|
const { generateCert, getCertForDomain } = require("./cert/generate");
|
||||||
const { getMitmAlias } = require("./dbReader");
|
const { getMitmAlias } = require("./dbReader");
|
||||||
|
|
@ -311,9 +311,8 @@ const server = https.createServer(sslOptions, async (req, res) => {
|
||||||
const tool = getToolForHost(req.headers.host);
|
const tool = getToolForHost(req.headers.host);
|
||||||
if (!tool) return passthrough(req, res, bodyBuffer);
|
if (!tool) return passthrough(req, res, bodyBuffer);
|
||||||
|
|
||||||
const patterns = URL_PATTERNS[tool] || [];
|
// Kiro IDE posts chat to `/` with x-amz-target (not path /generateAssistantResponse)
|
||||||
const isChat = patterns.some(p => req.url.includes(p));
|
if (!isChatRequest(tool, req)) return passthrough(req, res, bodyBuffer);
|
||||||
if (!isChat) return passthrough(req, res, bodyBuffer);
|
|
||||||
|
|
||||||
// Cursor uses binary proto — model extraction not possible at this layer.
|
// Cursor uses binary proto — model extraction not possible at this layer.
|
||||||
// Delegate directly to handler which decodes proto internally.
|
// Delegate directly to handler which decodes proto internally.
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,13 @@ export const MITM_TOOLS = {
|
||||||
color: "#FF6B00",
|
color: "#FF6B00",
|
||||||
description: "Kiro IDE with MITM",
|
description: "Kiro IDE with MITM",
|
||||||
configType: "mitm",
|
configType: "mitm",
|
||||||
mitmDomain: "q.us-east-1.amazonaws.com",
|
mitmDomain: "runtime.us-east-1.kiro.dev",
|
||||||
defaultModels: [
|
defaultModels: [
|
||||||
|
// Kiro's agent/"vibe" mode sends modelId "auto" for the main turn and "simple-task"
|
||||||
|
// for background sub-tasks (verified via MITM request dump of generateAssistantResponse).
|
||||||
|
// Both need a mappable slot — otherwise getMappedModel returns null and the chat call
|
||||||
|
// is passed through to AWS instead of being routed to the chosen provider.
|
||||||
|
{ id: "auto", name: "Auto (Kiro Agent)", alias: "auto" },
|
||||||
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", alias: "claude-sonnet-5" },
|
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", alias: "claude-sonnet-5" },
|
||||||
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", alias: "claude-sonnet-4.5" },
|
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", alias: "claude-sonnet-4.5" },
|
||||||
{ id: "claude-sonnet-4", name: "Claude Sonnet 4", alias: "claude-sonnet-4" },
|
{ id: "claude-sonnet-4", name: "Claude Sonnet 4", alias: "claude-sonnet-4" },
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,13 @@ describe("Kiro MITM model slots", () => {
|
||||||
expect(Array.isArray(kiro.defaultModels)).toBe(true);
|
expect(Array.isArray(kiro.defaultModels)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("offers a mappable slot for the agent default model id 'auto'", () => {
|
||||||
|
// اسلات auto برای vibe mode لازمه — وگرنه درخواست میره AWS
|
||||||
|
const auto = kiro.defaultModels.find((m) => m.id === "auto");
|
||||||
|
expect(auto).toBeTruthy();
|
||||||
|
expect(auto.alias).toBe("auto");
|
||||||
|
});
|
||||||
|
|
||||||
it("offers a mappable slot for Claude Sonnet 5", () => {
|
it("offers a mappable slot for Claude Sonnet 5", () => {
|
||||||
const sonnet5 = kiro.defaultModels.find((m) => m.id === "claude-sonnet-5");
|
const sonnet5 = kiro.defaultModels.find((m) => m.id === "claude-sonnet-5");
|
||||||
expect(sonnet5).toBeTruthy();
|
expect(sonnet5).toBeTruthy();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue