fix: remove native question limits

This commit is contained in:
mesalogo
2026-09-08 21:52:10 +08:00
parent fbc0f5e9f3
commit 78418a56fa
6 changed files with 75 additions and 38 deletions
+5 -6
View File
@@ -120,14 +120,13 @@ const continueHostStreamEventSchema = z.discriminatedUnion('type', [
const continueHostQuestionSchema = z const continueHostQuestionSchema = z
.object({ .object({
requestId: z.string().min(1).max(128), requestId: z.string().min(1),
timestamp: z.number().finite().optional(), timestamp: z.number().finite().optional(),
question: z.string().trim().min(1).max(2_000), question: z.string().trim().min(1),
options: z options: z
.array(z.string().trim().min(1).max(200)) .array(z.string().trim().min(1))
.max(20)
.optional(), .optional(),
defaultAnswer: z.string().trim().max(2_000).optional() defaultAnswer: z.string().trim().optional()
}) })
.strict() .strict()
@@ -925,7 +924,7 @@ export class ContinueHostAdapter {
patched = replaceExactly( patched = replaceExactly(
patched, patched,
serverMarker, serverMarker,
'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&ue.length<=128&&typeof ce==="string"&&ce.length>0&&ce.length<=2e3?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"' 'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&typeof ce==="string"&&ce.length>0?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"'
) )
patched = replaceExactly( patched = replaceExactly(
patched, patched,
+40 -2
View File
@@ -2075,7 +2075,26 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
); );
it.each(["parent", "child"] as const)("parses %s OpenCode questions and sends selected answers back", async (owner) => { it.each(["parent", "child"] as const)("parses %s OpenCode questions and sends selected answers back", async (owner) => {
const longQuestion = `请选择实现方式\n${"发布说明内容。".repeat(6_000)}`;
const setup = runClient([ const setup = runClient([
{
id: "question-tool-running",
type: "message.part.updated",
properties: {
sessionID: "session-1",
part: {
id: "part-question-1",
callID: "call-question-1",
type: "tool",
tool: "question",
state: {
status: "running",
input: { questions: [] },
time: { start: 1 },
},
},
},
},
{ {
id: "question-event", id: "question-event",
type: "question.asked", type: "question.asked",
@@ -2085,7 +2104,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
questions: [ questions: [
{ {
header: "实现方式", header: "实现方式",
question: "请选择实现方式", question: longQuestion,
options: [ options: [
{ {
label: "直接修改", label: "直接修改",
@@ -2106,6 +2125,25 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
}, },
}, },
}, },
{
id: "question-tool-completed",
type: "message.part.updated",
properties: {
sessionID: "session-1",
part: {
id: "part-question-1",
callID: "call-question-1",
type: "tool",
tool: "question",
state: {
status: "completed",
input: { questions: [] },
output: "Answered",
time: { start: 1, end: 2 },
},
},
},
},
{ {
id: "idle", id: "idle",
type: "session.idle", type: "session.idle",
@@ -2136,7 +2174,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
questions: [ questions: [
{ {
header: "实现方式", header: "实现方式",
question: "请选择实现方式", question: longQuestion,
multiple: false, multiple: false,
custom: true, custom: true,
}, },
+4 -24
View File
@@ -81,9 +81,6 @@ const MAX_PERMISSION_PATTERN_LENGTH = 1_024;
const MAX_PERMISSION_PATTERNS_BYTES = 8 * 1_024; const MAX_PERMISSION_PATTERNS_BYTES = 8 * 1_024;
const MAX_PERMISSION_METADATA_BYTES = 8 * 1_024; const MAX_PERMISSION_METADATA_BYTES = 8 * 1_024;
const MAX_EXECUTION_OUTPUT_BYTES = 1024 * 1024; const MAX_EXECUTION_OUTPUT_BYTES = 1024 * 1024;
const MAX_QUESTION_REQUEST_BYTES = 32 * 1_024;
const MAX_QUESTIONS_PER_REQUEST = 4;
const MAX_QUESTION_OPTIONS = 20;
const MAX_NATIVE_AGENTS = runtimeNativeInventoryLimits.agents; const MAX_NATIVE_AGENTS = runtimeNativeInventoryLimits.agents;
const MAX_NATIVE_TOOLS = runtimeNativeInventoryLimits.tools; const MAX_NATIVE_TOOLS = runtimeNativeInventoryLimits.tools;
const MAX_NATIVE_COMMANDS = runtimeNativeInventoryLimits.commands; const MAX_NATIVE_COMMANDS = runtimeNativeInventoryLimits.commands;
@@ -384,29 +381,22 @@ function parseQuestionRequest(
if ( if (
typeof id !== "string" || typeof id !== "string" ||
id.length === 0 || id.length === 0 ||
id.length > MAX_PERMISSION_NAME_LENGTH ||
!Array.isArray(questions) || !Array.isArray(questions) ||
questions.length === 0 || questions.length === 0 ||
questions.length > MAX_QUESTIONS_PER_REQUEST ||
!questions.every( !questions.every(
(question) => (question) =>
isRecord(question) && isRecord(question) &&
typeof question.question === "string" && typeof question.question === "string" &&
question.question.trim().length > 0 && question.question.trim().length > 0 &&
question.question.length <= 2_000 &&
typeof question.header === "string" && typeof question.header === "string" &&
question.header.trim().length > 0 && question.header.trim().length > 0 &&
question.header.length <= 120 &&
Array.isArray(question.options) && Array.isArray(question.options) &&
question.options.length <= MAX_QUESTION_OPTIONS &&
question.options.every( question.options.every(
(option) => (option) =>
isRecord(option) && isRecord(option) &&
typeof option.label === "string" && typeof option.label === "string" &&
option.label.trim().length > 0 && option.label.trim().length > 0 &&
option.label.length <= 200 && typeof option.description === "string",
typeof option.description === "string" &&
option.description.length <= 1_000,
) && ) &&
(question.multiple === undefined || (question.multiple === undefined ||
typeof question.multiple === "boolean") && typeof question.multiple === "boolean") &&
@@ -416,22 +406,11 @@ function parseQuestionRequest(
(!isRecord(tool) || (!isRecord(tool) ||
typeof tool.messageID !== "string" || typeof tool.messageID !== "string" ||
tool.messageID.length === 0 || tool.messageID.length === 0 ||
tool.messageID.length > 256 ||
typeof tool.callID !== "string" || typeof tool.callID !== "string" ||
tool.callID.length === 0 || tool.callID.length === 0))
tool.callID.length > 256))
) { ) {
throw new Error("OpenCode 提问请求格式无效"); throw new Error("OpenCode 提问请求格式无效");
} }
let serialized: string;
try {
serialized = JSON.stringify(properties);
} catch {
throw new Error("OpenCode 提问请求无法序列化");
}
if (!byteLengthWithin(serialized, MAX_QUESTION_REQUEST_BYTES)) {
throw new Error("OpenCode 提问请求超过安全限制");
}
return properties as QuestionRequest; return properties as QuestionRequest;
} }
@@ -2569,6 +2548,7 @@ export class OpenCodeRuntime implements AgentRuntime {
throw new Error("OpenCode 工具调用 ID 格式无效"); throw new Error("OpenCode 工具调用 ID 格式无效");
} }
const toolName = part.tool.slice(0, 200); const toolName = part.tool.slice(0, 200);
const nativeControlTool = toolName.trim().toLowerCase() === "question";
const state = const state =
part.state.status === "error" ? "failed" : part.state.status; part.state.status === "error" ? "failed" : part.state.status;
if (state === "failed") { if (state === "failed") {
@@ -2609,7 +2589,7 @@ export class OpenCodeRuntime implements AgentRuntime {
const retained = subagentProgress.retain(subagent); const retained = subagentProgress.retain(subagent);
toolStates.get(callId)!.subagent = retained; toolStates.get(callId)!.subagent = retained;
yield retained; yield retained;
} else { } else if (!nativeControlTool) {
yield { yield {
requestId: request.requestId, requestId: request.requestId,
type: "tool", type: "tool",
-1
View File
@@ -122,7 +122,6 @@ export function AgentQuestionCard({
<span>{t('question.otherAnswer')}</span> <span>{t('question.otherAnswer')}</span>
<input <input
disabled={submitting} disabled={submitting}
maxLength={2_000}
onChange={(event) => { onChange={(event) => {
const answer = event.target.value const answer = event.target.value
setCustom((current) => setCustom((current) =>
+23 -1
View File
@@ -15,7 +15,10 @@ import {
projectExecutionSpaceSchema, projectExecutionSpaceSchema,
projectUpdateSchema projectUpdateSchema
} from './assistant-contracts' } from './assistant-contracts'
import { subagentEventSchema } from './contracts' import {
agentQuestionResponseSchema,
subagentEventSchema
} from './contracts'
const untouchedProject: AssistantProject = { const untouchedProject: AssistantProject = {
id: '00000000-0000-4000-8000-000000000101', id: '00000000-0000-4000-8000-000000000101',
@@ -34,6 +37,25 @@ const untouchedProject: AssistantProject = {
updatedAt: '2026-08-01T00:00:00.000Z' updatedAt: '2026-08-01T00:00:00.000Z'
} }
describe('agent question contracts', () => {
it('does not impose GoodBuddy-specific question answer limits', () => {
const answers = Array.from({ length: 5 }, (_, questionIndex) =>
Array.from(
{ length: 21 },
(_, optionIndex) =>
`${questionIndex}:${optionIndex}:${'回答内容'.repeat(600)}`
)
)
expect(
agentQuestionResponseSchema.parse({
questionId: `question-${'id'.repeat(100)}`,
answers
}).answers
).toEqual(answers)
})
})
describe('activity history contracts', () => { describe('activity history contracts', () => {
it('accepts more than 500 records and details longer than 4,000 characters', () => { it('accepts more than 500 records and details longer than 4,000 characters', () => {
const records = Array.from({ length: 501 }, (_, index) => ({ const records = Array.from({ length: 501 }, (_, index) => ({
+3 -4
View File
@@ -305,13 +305,12 @@ export const workspaceOpenPathRequestSchema = z
.strict() .strict()
export const agentQuestionAnswerSchema = z export const agentQuestionAnswerSchema = z
.array(z.string().trim().min(1).max(2_000)) .array(z.string().trim().min(1))
.max(20)
export const agentQuestionResponseSchema = z export const agentQuestionResponseSchema = z
.object({ .object({
questionId: z.string().trim().min(1).max(128), questionId: z.string().trim().min(1),
answers: z.array(agentQuestionAnswerSchema).max(4) answers: z.array(agentQuestionAnswerSchema)
}) })
.strict() .strict()