diff --git a/doc/architecture/runner-cursor-capabilities.md b/doc/architecture/runner-cursor-capabilities.md index d63ec6e8073..9f7312ac6d7 100644 --- a/doc/architecture/runner-cursor-capabilities.md +++ b/doc/architecture/runner-cursor-capabilities.md @@ -21,7 +21,7 @@ The reference is the runner's Codex app-server integration and its closed thread | Streaming assistant/reasoning | `agent_message_chunk`, `agent_thought_chunk` in live and replay presenters | Existing ACP streaming path | Authenticated event capture required | | Tool lifecycle/input/output | `tool_call` and `tool_call_update`, including raw input/output, locations and content | Existing canonical tool lifecycle; Cursor extensions add semantic activity | Standard content blocks/diffs must survive shared ACPX mapping; evaluate captured field coverage | | Human questions | `cursor/ask_question`, native single/multiple selections | `normalizeCursorQuestionRequest`: all questions required; opaque UI IDs round-trip native IDs; forged/duplicate/missing selections rejected | Cancellation and skip outcomes must be routed by shared durable request lifecycle | -| Proposed plans and decisions | `cursor/create_plan` with full Markdown, overview, todos, named phases and project flag | `normalizeCursorPlanRequest`: complete presentation; accept/reject/cancel; revision-derived question ID; rejection feedback | Shared question description must allow 100,000 chars. Oversize rejects; it is never truncated. No generated `planUri`, since no uploaded plan artifact is fabricated | +| Proposed plans and decisions | `cursor/create_plan` with full Markdown, overview, todos, named phases and project flag | `normalizeCursorPlanRequest`: complete presentation; accept/reject/cancel; revision-derived question ID; rejection feedback | Foundation question descriptions allow 100,000 chars; this boundary also enforces the 196 KiB UTF-8 durable payload cap. Oversize rejects; it is never truncated. No generated `planUri`, since no uploaded plan artifact is fabricated | | Execution checklist | Standard ACP `plan` and `cursor/update_todos` (`merge`) | Per-turn reducer merges by native ID and emits complete snapshots with increasing revisions | Native cancelled status is visible as “Cancelled” with canonical blocked status, since canonical plan schema has no cancelled enum | | Child activity | Optional `subagent_spawned`, `subagent_state_update`; metadata has tool-call/agent/model identity | Stateful child normalizer emits canonical delegation and retains role/task across state deltas; shared host must validate parent ownership | Requires client `_meta.subagents: true`. Nested transcript routing and child authority need end-to-end evidence | | Completed subagent tasks | `cursor/task`: description, prompt, subtype, model, ID, duration | Canonical delegation with role, model, task and duration in activity summary | Dedicated numeric duration field absent from canonical delegation schema | @@ -66,6 +66,8 @@ Passed in the provider worktree: - Targeted strict TypeScript compilation for both provider modules and their imported contracts. - Credential-free wire initialization and auth/method probes, plus real macOS ARM64 materialization and archive/closure inventories for the other two platforms. -Priority follow-ups before advertising support: (1) shared hook integration with durable response/recovery tests; (2) descriptor/private-snapshot native distribution launch and guardian cleanup; (3) explicit credentials plus exact available model and measured dollar budget; (4) local and Daytona semantic tools, permissions, steering distinction, files/artifacts, questions/plans and settlement proof; (5) field-by-field captured trace audit to identify any additional unconsumed native fields. +The generic native-distribution lease now snapshots the pinned closure, launches Cursor through its verified bundled Node with a closed-module guard, and uses the existing lifetime guardian. A real nonbillable Cursor initialization passed through that lease. Its six focused tests passed, including argument fencing, external module rejection, and native child ownership/exit proof. The existing snapshot suite passed; four legacy installation tests were blocked by the shared installed Claude SDK (0.3.280 versus the pinned 0.3.263), with the remaining legacy tests passing or platform-skipped. + +Priority follow-ups before advertising support: (1) shared hook integration with durable response/recovery tests; (2) authenticated native lifetime and cleanup proof; (3) explicit credentials plus exact available model and measured dollar budget; (4) local and Daytona semantic tools, permissions, steering distinction, files/artifacts, questions/plans and settlement proof; (5) field-by-field captured trace audit to identify any additional unconsumed native fields. Remaining rich capabilities intentionally unused: session discovery, generic mode/config-option UI, provider session titles, command/skill picker metadata, nested child transcripts beyond delegation summaries, native numeric task duration, and optional planUri. They are listed above with reasons; neither “unsupported” nor “complete” should be inferred for unverified live behaviors. The final combined report must update this inventory with integration evidence and include any further events observed during authenticated qualification. diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts index 90de217b4d6..439a5f0c830 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts @@ -52,6 +52,9 @@ describe("Cursor blocking extension normalization", () => { it("never truncates or fabricates approval of an oversized plan", () => { expect(() => normalizeCursorPlanRequest({ toolCallId: "plan", plan: "x".repeat(100_001), todos: [] })).toThrow(); expect(() => normalizeCursorPlanRequest({ toolCallId: "plan", plan: "x", todos: [{ id: "1", content: "x", status: "mystery" }] })).toThrow(); + const full = "Details\n".repeat(1_000); + expect(normalizeCursorPlanRequest({ toolCallId: "plan", plan: full, todos: [] }).questionSet.description).toBe(full); + expect(() => normalizeCursorPlanRequest({ toolCallId: "plan", plan: "漢".repeat(90_000), todos: [] })).toThrow("byte bound"); }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts index 3ba52a0ad2a..0b6727a1257 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts @@ -70,7 +70,7 @@ export function normalizeCursorQuestionRequest(value: unknown): { }, }; }); - const questionSet = parsePaperclipQuestionSet({ + const questionSet = boundedQuestionSet({ schema: PAPERCLIP_QUESTION_SET_SCHEMA, title: optionalText(request.title, "title", 1_000) ?? "Cursor needs input", questions: bindings.map(({ question }) => question), @@ -120,7 +120,7 @@ export function normalizeCursorPlanRequest(value: unknown): { requiredText(description, "complete plan presentation", 100_000); const revision = createHash("sha256").update(JSON.stringify({ toolCallId, name, overview, plan, todos, phases, isProject: request.isProject ?? false })).digest("hex"); const questionId = `plan-${revision}`; - const questionSet = parsePaperclipQuestionSet({ + const questionSet = boundedQuestionSet({ schema: PAPERCLIP_QUESTION_SET_SCHEMA, title: name ?? "Review Cursor's plan", description, submitLabel: "Send decision", questions: [{ id: questionId, prompt: "How should Cursor proceed with this plan?", required: true, @@ -271,6 +271,12 @@ function notice(id: string, category: string, summary: string, severity: "info" scope: "turn", recoverable: true, userActionable: false, summary, } }; } +function boundedQuestionSet(value: unknown): PaperclipQuestionSet { + const parsed = parsePaperclipQuestionSet(value); + // Match the durable Rust question payload bound, including UTF-8 expansion. + if (Buffer.byteLength(JSON.stringify(parsed)) > 196 * 1024) throw new Error("Cursor question presentation exceeds the durable input byte bound"); + return parsed; +} function parseTodos(value: unknown): Todo[] { const seen = new Set(); return array(value, "todos", 256).map(raw => {