fix(cursor): preserve mainline contracts during selective integration

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-03 14:03:14 -05:00
1 parent f71957ef76
commit ae24f09812
11 files changed
+45 -123

No files matched your search

@@ -170,3 +170,17 @@ Historical unbound committed proofs retain their original event selection and
1,000-row limit. Long-plan tests also reject foreign tools, changed sessions,
tampered digests, and progress after completion; no later work is hidden by
removing progress from the proof.
The first new denied-write attempt retained a native permission for a different
command: Cursor escaped the literal's underscores. The original target stayed
absent and the observed process tree retired, but the exact command/denial gate
correctly failed. The fixture now uses a hyphenated literal while retaining exact
command digest, tool, request, run, and turn checks. This changes the fixture
identity and does not qualify the failed attempt retroactively.
CI found an imported editable-default question test without its corresponding
contract. Editable defaults are outside this Cursor release and their partial
import was removed instead of extending all question surfaces. Cursor question
handling and plans remain intact. Configuration now preserves mainline behavior
for unavailable Pi/Copilot profiles while retaining Cursor's explicit model and
mode selection.
@@ -241,6 +241,22 @@ describe("buildPaperclipRunnerConfig", () => {
});
});
it.each(["agent", "plan", "ask"])("preserves Cursor's explicit model and %s mode", (mode) => {
expect(buildPaperclipRunnerConfig(makeValues({
model: "explicit-cursor-model",
adapterSchemaValues: { provider: "acpx", acpxAgent: "cursor", acpxSessionMode: mode },
}))).toMatchObject({ provider: "acpx", acpxAgent: "cursor", model: "explicit-cursor-model", acpxSessionMode: mode });
});
it("defaults Cursor to Agent and requires an explicit model", () => {
expect(buildPaperclipRunnerConfig(makeValues({
model: "explicit-cursor-model", adapterSchemaValues: { provider: "acpx", acpxAgent: "cursor" },
}))).toMatchObject({ acpxAgent: "cursor", acpxSessionMode: "agent", model: "explicit-cursor-model" });
expect(() => buildPaperclipRunnerConfig(makeValues({
model: "", adapterSchemaValues: { provider: "acpx", acpxAgent: "cursor" },
}))).toThrow("cursor requires an explicit provider model");
});
it("builds a Claude Managed profile reference with explicit retention and spend controls", () => {
const config = buildPaperclipRunnerConfig(makeValues({
adapterType: "paperclip_runner",
@@ -97,7 +97,7 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
const provider = isPaperclipRunnerProvider(providerCandidate)
? providerCandidate
: "codex";
const acpxAgent = PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === schemaValues.acpxAgent)?.value ?? "claude";
const acpxAgent = PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === schemaValues.acpxAgent && (profile.qualified || profile.value === "cursor"))?.value ?? "claude";
const cursorMode = resolvePaperclipRunnerCursorMode(provider, acpxAgent, schemaValues.acpxSessionMode);
const schemaModel = typeof schemaValues.model === "string"
@@ -106,7 +106,7 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
const configuredModel = typeof config.model === "string"
? config.model.trim()
: "";
if (provider === "acpx" && ["cursor", "copilot", "pi"].includes(acpxAgent) && !configuredModel && !schemaModel) {
if (provider === "acpx" && acpxAgent === "cursor" && !configuredModel && !schemaModel) {
throw new Error(`${acpxAgent} requires an explicit provider model`);
}
const managedProfileId = typeof schemaValues.managedProfileId === "string"
@@ -406,7 +406,7 @@ fn sanitize_question_set(mut value: Value) -> Result<Value, LocalRunnerError> {
};
// IDs, answer modes, and validation patterns are protocol values:
// changing them would break response correlation or semantics.
redact_object_text(question, &["header", "prompt", "helpText", "initialText"]);
redact_object_text(question, &["header", "prompt", "helpText"]);
if let Some(options) = question.get_mut("options").and_then(Value::as_array_mut) {
for option in options {
if let Some(option) = option.as_object_mut() {
@@ -643,15 +643,6 @@ pub(crate) fn validate_question_set(value: &Value) -> Result<(), LocalRunnerErro
"ACPX input question ids must be unique",
));
}
if question
.get("initialText")
.and_then(Value::as_str)
.is_some_and(|text| text.chars().count() > 100_000)
{
return Err(LocalRunnerError::invalid(
"ACPX initial text exceeds its Unicode code-point bound",
));
}
let mut option_ids = BTreeSet::new();
for option in question
.get("options")
@@ -3128,30 +3128,6 @@ mod tests {
);
}
#[test]
fn durable_question_initial_text_survives_serialized_reload_without_resolution() {
let mut config = config(PathBuf::from("unused"));
config.max_outbox_bytes = 512 * 1024;
config.max_frame_bytes = 256 * 1024;
let mut state = DurableState::new(&config);
let text = " Editable draft 漢字\n".repeat(1_000);
state.enqueue_event(&config, "runtime_request.created", EventPriority::P0, json!({
"request": {"schema":"paperclip.runtime_request.v2", "requestKind":"runtime", "requestId":"edit-1",
"type":"input", "status":"pending", "input":{"schema":"paperclip.question_set.v1", "questions":[{
"id":"draft", "prompt":"Edit", "answerMode":"text", "required":true, "initialText":text
}]}}
})).unwrap();
let reloaded: DurableState =
serde_json::from_slice(&serde_json::to_vec(&state).unwrap()).unwrap();
let request = reloaded.outbox[0]
.envelope
.pointer("/payload/payload/request")
.unwrap();
assert_eq!(request["input"]["questions"][0]["initialText"], json!(text));
assert_eq!(request["status"], "pending");
assert!(request.get("response").is_none());
}
#[test]
fn durable_question_sets_preserve_safe_identity_and_redact_display_text() {
let config = config(PathBuf::from("unused"));
@@ -402,80 +402,6 @@ fn rejects_payloads_before_decoding_when_scope_or_size_is_invalid() {
assert!(error.to_string().contains("256 KiB"));
}
#[test]
fn input_initial_text_survives_wire_admission_with_existing_redaction() {
let scope = active_scope();
let input = |initial: Value, mode: &str| {
json!({"requestId":"request-1","questionSet":{
"schema":"paperclip.question_set.v1","questions":[{"id":"draft","prompt":"Edit", "required":true,
"answerMode":mode,"initialText":initial}]
}})
};
let text = " Draft 漢字\n".repeat(1_000);
let decoded = decode_acpx_event(
&scope,
&event(
GeneratedAcpxSidecarEventType::RuntimeInputRequested,
input(json!(text), "text"),
),
)
.unwrap();
match decoded {
AcpxEventPayload::InputRequested { question_set, .. } => {
assert_eq!(question_set["questions"][0]["initialText"], json!(text))
}
_ => panic!("input request expected"),
}
let decoded = decode_acpx_event(
&scope,
&event(
GeneratedAcpxSidecarEventType::RuntimeInputRequested,
input(json!("token=private-prefill"), "text"),
),
)
.unwrap();
match decoded {
AcpxEventPayload::InputRequested { question_set, .. } => {
assert_eq!(
question_set["questions"][0]["initialText"],
"token=[REDACTED]"
);
assert!(question_set["description"]
.as_str()
.unwrap()
.contains("redacted"));
}
_ => panic!("input request expected"),
}
for code_points in [100_000, 100_001] {
let initial = format!("{}😀", "a".repeat(code_points - 1));
let payload = input(json!(initial), "text");
assert!(serde_json::to_vec(&payload).unwrap().len() < 196 * 1024);
let result = decode_acpx_event(
&scope,
&event(
GeneratedAcpxSidecarEventType::RuntimeInputRequested,
payload,
),
);
assert_eq!(result.is_ok(), code_points == 100_000);
}
for invalid in [
input(json!(1), "text"),
input(json!("a".repeat(100_001)), "text"),
input(json!("default"), "single_select"),
] {
assert!(decode_acpx_event(
&scope,
&event(
GeneratedAcpxSidecarEventType::RuntimeInputRequested,
invalid
)
)
.is_err());
}
}
#[test]
fn native_plan_parent_tool_is_bounded_origin_scoped_and_not_redacted() {
let input = json!({"requestId":"input-1","toolCallId":"tool with spaces",
@@ -273,7 +273,15 @@ fn check_tool_receiver_admission(oversized: bool) {
assert!(session.state().pending_tool("call-admission").is_some());
session.deliver_tool_result(&result).unwrap();
assert!(!session.state().has_pending_tools());
assert!(session.deliver_tool_result(&result).is_err());
// An exact durable delivery replay is acknowledged without a second
// sidecar resolution. The command journal below proves that boundary.
session.deliver_tool_result(&result).unwrap();
let mut changed = result.clone();
changed.result = json!({"id":"different-issue"});
assert!(session.deliver_tool_result(&changed).is_err());
changed = result.clone();
changed.operation_id = "issues.update".to_owned();
assert!(session.deliver_tool_result(&changed).is_err());
}
session.shutdown("admission test complete").unwrap();
let rows: Vec<serde_json::Value> = std::fs::read_to_string(&journal)
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
const CODEX_ACPX_DIGEST: &str =
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3";
const PI_ACPX_DIGEST: &str =
"sha256:5e1a4357fc108fa79a66111413f85f6353b3dd3ab802ddfb80fc66719bc12571";
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f";
fn temporary_directory(label: &str) -> PathBuf {
let nonce = SystemTime::now()
@@ -148,7 +148,6 @@ function normalizeField(
question: {
...base,
answerMode: "text",
...(property.default !== undefined ? { initialText: initialText(property.default, type) } : {}),
textValidation: {
inputType: "text",
...(minLength !== undefined ? { minLength } : {}),
@@ -169,7 +168,6 @@ function normalizeField(
question: {
...base,
answerMode: "text",
...(property.default !== undefined ? { initialText: initialText(property.default, type) } : {}),
textValidation: {
inputType: type,
...(minimum !== undefined ? { minimum } : {}),
@@ -378,10 +376,3 @@ function finiteNonNegativeInteger(value: unknown): number | undefined {
? Number(value)
: undefined;
}
/** Defaults are editable presentation, not acceptance or native response content. */
function initialText(value: unknown, type: string): string {
if (type === "string" && typeof value === "string") return value;
if ((type === "number" || type === "integer") && typeof value === "number" && Number.isFinite(value)) return String(value);
throw new Error(`ACP ${type} text field default has an incompatible type`);
}
+1 -1
View File
@@ -40,7 +40,7 @@ export function readCursorToolEvidence(rows: readonly unknown[], runId: string):
/** Exact absolute target removes any dependency on implicit native shell cwd. */
export function cursorDeniedCommand(path: string) {
if (!isAbsolute(path) || /[\u0000-\u001f\u007f]/u.test(path)) throw new Error("Invalid denial target");
const command = `printf 'MUST_NOT_EXIST' > '${path.replaceAll("'", "'\\''")}'`;
const command = `printf 'MUST-NOT-EXIST' > '${path.replaceAll("'", "'\\''")}'`;
return { command, commandSha256: `sha256:${createHash("sha256").update(command).digest("hex")}` };
}
export function hasCursorDeniedCommand(input: {
+1 -1
View File
@@ -43,7 +43,7 @@ it("awaits owned remote baseline before returning the exact remote denial comman
const prepared = await prepareCursorRemoteAction({ fixture, ...binding, deniedRelative: "denied.txt", prompt: "Perform the actual test" });
order.push("publish-action");
expect(order).toEqual(["before-action-publication", "baseline-ready", "publish-action"]);
expect(prepared.prompt).toContain("printf 'MUST_NOT_EXIST' > '/home/daytona/paperclip-workspace/denied.txt'");
expect(prepared.prompt).toContain("printf 'MUST-NOT-EXIST' > '/home/daytona/paperclip-workspace/denied.txt'");
expect(prepared.initial).toMatchObject({ phase: "before-request", absent: true });
expect(prepared.command?.commandSha256).toMatch(/^sha256:[a-f0-9]{64}$/);
await expect(prepareCursorRemoteAction({ fixture, ...binding, runId: "wrong-run", deniedRelative: "denied.txt", prompt: "test" })).rejects.toThrow(/another run/);