Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/pi-plugin/src/issue-880-cursor-receipt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { expect, test } from "bun:test";
import { createHash } from "node:crypto";
import {
piConversationFields,
piCursorManagedReceipt,
piHasManagedSignature,
} from "./pi-dispatch-identity";

const signature = "<session-history>";
const managed = [
{
role: "user",
content: [{ type: "text", text: `${signature}prepared</session-history>` }],
},
{ role: "user", content: "older" },
];
const root = [
{ role: "system", content: "instructions" },
{
role: "user",
content: [{ type: "text", text: `${signature}prepared</session-history>` }],
},
{ role: "user", content: [{ type: "text", text: "older" }] },
];
const rootIds = root.map((message) =>
createHash("sha256").update(JSON.stringify(message), "utf8").digest(),
);

function cursorPayload(activeText = "current prompt", messageId = "random-1") {
return {
conversationState: {
rootPromptMessagesJson: rootIds,
turns: [],
},
action: {
action: {
case: "userMessageAction",
value: {
userMessage: { text: activeText, messageId },
},
},
},
};
}

test("issue 880 cursor-agent: managed root blobs are accepted through the receipt", () => {
const receipt = piCursorManagedReceipt(managed);
expect(receipt).toHaveLength(1);
expect(piHasManagedSignature(cursorPayload(), signature, receipt)).toBe(true);
expect(piHasManagedSignature(cursorPayload(), signature)).toBe(false);
});

test("issue 880 cursor-agent: a missing managed root blob is rejected", () => {
const receipt = piCursorManagedReceipt(managed);
const payload = cursorPayload();
payload.conversationState.rootPromptMessagesJson = [rootIds[0], rootIds[2]];
expect(piHasManagedSignature(payload, signature, receipt)).toBe(false);
});

test("issue 880 cursor-agent: inline root JSON remains readable", () => {
const payload = cursorPayload();
payload.conversationState.rootPromptMessagesJson = root.map((message) =>
Buffer.from(JSON.stringify(message), "utf8"),
);
expect(piHasManagedSignature(payload, signature)).toBe(true);
});

test("issue 880 cursor-agent: action identity ignores Cursor's random message id", () => {
const fields = piConversationFields(cursorPayload());
expect(fields).toBeDefined();
expect(
piConversationFields(cursorPayload("current prompt", "random-2")),
).toEqual(fields);
expect(piConversationFields(cursorPayload("a different turn"))).not.toEqual(
fields,
);
});

test("issue 880 cursor-agent: standard message payloads still work", () => {
const payload = {
messages: [
{ role: "user", content: `${signature}managed</session-history>` },
],
};
expect(piHasManagedSignature(payload, signature)).toBe(true);
expect(piConversationFields(payload)).toBeDefined();
});
105 changes: 105 additions & 0 deletions packages/pi-plugin/src/issue-880-provider-fence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { afterEach, expect, test } from "bun:test";
import { createHash } from "node:crypto";
import {
clearPiContextReceipt,
registerPiGuardedContext,
} from "./pi-context-refusal";
import { piCursorManagedReceipt } from "./pi-dispatch-identity";
import { __setPiHarnessKindForTesting } from "./pi-harness-kind";
import { fakeContext, userMessage } from "./test-utils.test";

const session = "issue-880";
const signature = "<session-history>";
const managed = [
userMessage(`${signature}prepared</session-history>`, 1),
userMessage("current prompt", 2),
];

function cursorPayload() {
const receipt = piCursorManagedReceipt(managed);
if (!receipt) throw new Error("managed Cursor receipt missing");
const system = createHash("sha256")
.update(JSON.stringify({ role: "system", content: "instructions" }), "utf8")
.digest("hex");
return {
conversationState: {
rootPromptMessagesJson: [system, ...receipt],
turns: [],
},
action: {
action: {
case: "userMessageAction",
value: {
userMessage: {
text: "current prompt",
messageId: "random-cursor-message-id",
},
},
},
},
};
}

function harness() {
__setPiHarnessKindForTesting("omp");
const handlers = new Map<string, (event: unknown, ctx: unknown) => unknown>();
const refusals: string[] = [];
let aborts = 0;
const ctx = {
...fakeContext(session),
model: { api: "cursor-agent", id: "claude-opus-5-5", provider: "cursor" },
abort: () => aborts++,
};
registerPiGuardedContext(
{
on: (name: string, handler: (event: unknown, ctx: unknown) => unknown) =>
handlers.set(name, handler),
appendEntry: (_type: string, data: { message: string }) =>
refusals.push(data.message),
} as never,
async () => ({ messages: structuredClone(managed) }),
);
const emit = (name: string, event: unknown = {}) => {
const handler = handlers.get(name);
if (!handler) throw new Error(`missing handler ${name}`);
return handler(event, ctx);
};
return {
refusals,
get aborts() {
return aborts;
},
async prepare() {
emit("agent_start");
await emit("context", { messages: [userMessage("current prompt", 2)] });
},
dispatch(payload: unknown) {
return emit("before_provider_request", { payload });
},
};
}

afterEach(() => {
clearPiContextReceipt(session);
__setPiHarnessKindForTesting(undefined);
});

test("issue 880 cursor-agent: managed AgentRunRequest is admitted", async () => {
const h = harness();
await h.prepare();
h.dispatch(cursorPayload());
expect(h.aborts).toBe(0);
expect(h.refusals).toEqual([]);
});

test("issue 880 cursor-agent: a raw AgentRunRequest without the receipt is refused", async () => {
const h = harness();
await h.prepare();
const payload = cursorPayload();
payload.conversationState.rootPromptMessagesJson = [
payload.conversationState.rootPromptMessagesJson[0],
];
h.dispatch(payload);
expect(h.aborts).toBe(1);
expect(h.refusals[0]).toContain("missing current receipt signature");
});
31 changes: 23 additions & 8 deletions packages/pi-plugin/src/pi-context-refusal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from "./pi-dispatch-chain";
import {
piConversationFields as conversationFields,
piCursorManagedReceipt,
piHasManagedSignature,
piManagedSignature,
} from "./pi-dispatch-identity";
Expand Down Expand Up @@ -81,6 +82,7 @@ interface Operation {
dispatchFields?: readonly LkgContentField[];
chainDispatchFields?: readonly LkgContentField[];
managedSignature?: string;
managedCursorReceipt?: readonly string[];
preparedStructure?: PiPreparedStructure;
// Recorded only for serializers that can chain through previous_response_id.
preparedHistory?: PiPreparedHistory;
Expand Down Expand Up @@ -139,6 +141,7 @@ function releasePiOperationPayloads(operation: Operation): void {
operation.dispatchFields = undefined;
operation.chainDispatchFields = undefined;
operation.managedSignature = undefined;
operation.managedCursorReceipt = undefined;
operation.preparedStructure = undefined;
operation.preparedHistory = undefined;
}
Expand Down Expand Up @@ -661,7 +664,13 @@ export function registerPiGuardedContext(
checked = expanded.payload;
chainInput = expanded.input;
}
if (!piHasManagedSignature(checked, active.managedSignature))
if (
!piHasManagedSignature(
checked,
active.managedSignature,
active.managedCursorReceipt,
)
)
throw new Error(
"provider request does not match this pass's managed dispatch (missing current receipt signature)",
);
Expand All @@ -679,13 +688,15 @@ export function registerPiGuardedContext(
// are not detected on this pass when no structural plan can be built.
const body = checked as { messages?: unknown; input?: unknown };
const skipReason = active.preparedStructure
? (active.preparedStructure.structuralSkipReason ??
(active.preparedStructure.api &&
body?.messages === undefined &&
Array.isArray(body?.input) &&
!active.preparedStructure.responsesMessages
? "provider selected Responses items without a matching history plan"
: undefined))
? active.preparedStructure.api === "cursor-agent"
? "Cursor AgentRunRequest root history is not modeled"
: (active.preparedStructure.structuralSkipReason ??
(active.preparedStructure.api &&
body?.messages === undefined &&
Array.isArray(body?.input) &&
!active.preparedStructure.responsesMessages
? "provider selected Responses items without a matching history plan"
: undefined))
: "prepared history representation is not modeled";
if (chained && skipReason)
throw new Error(
Expand Down Expand Up @@ -883,6 +894,10 @@ export function registerPiGuardedContext(
if (prepared.messages !== result.messages)
result.messages = prepared.messages;
active.managedSignature = piManagedSignature(result.messages);
active.managedCursorReceipt =
ctx.model?.api === "cursor-agent"
? piCursorManagedReceipt(result.messages)
: undefined;
active.preparedStructure = prepared.structure;
if (
fenceEnabled &&
Expand Down
Loading
Loading