Admit explicitly assigned Pi HTTPS tool gateways

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 15:14:57 -05:00
1 parent 82a0e7a069
commit 89644566f6
6 files changed
+51 -15

No files matched your search

+2 -2
View File
@@ -23,7 +23,7 @@ boundaries explicit.
| Native tools | `read`, `grep`, `find`, `ls`, `write`, `edit`, and `bash` pass the immutable extension gate before execution. File roots and read-only mode are checked before and after a permission wait. The host sandbox remains authoritative for shell commands and races. |
| Permissions | Native tool approval uses ACP `session/request_permission`, with allow once, allow for the session, and deny. Only offered options are accepted. Session grants cover an identical operation and still revalidate paths. Questions never become approvals. |
| Questions | Pi `select`, `confirm`, `input`, and `editor` map to ACP form elicitation with typed schemas. Decline, cancellation, timeout, malformed replies, and duplicate/late replies cannot become accepted answers. |
| Semantic tools | Runner-owned loopback HTTP MCP catalogs register under exact `mcp__<server>__<tool>` names. Calls retain the native tool call ID and cancellation signal. Authenticated PRP tool handling owns semantic authorization and durable interactions. Ambient MCP and external MCP servers are not admitted. |
| Semantic tools | Runner-bound HTTP MCP catalogs (numeric loopback HTTP or assigned HTTPS gateways) register under exact `mcp__<server>__<tool>` names. Calls retain the native tool call ID and cancellation signal. Authenticated PRP tool handling owns semantic authorization and durable interactions. Only the exact session-assigned gateway URL and credential are used, with redirects disabled. Ambient and unassigned MCP servers are not admitted. |
| Plans and artifacts | Pi has no native structured plan or artifact channel. Paperclip plan and artifact semantic tools remain available through the MCP bridge; native file edits retain bounded, workspace-confined ACP diff projection. Tool text/image results are preserved, and resource blocks are recorded without following URLs. |
| Steering | Capability-negotiated `pi/steer` issues native RPC `steer` during an active turn. `pi/follow_up` explicitly queues native RPC `follow_up`. Neither is inferred from a second ACP prompt. Each takes `{sessionId, message}` and returns `{accepted: true}`. |
| Usage | Prompt results sum actual assistant message usage receipts across continuations. Input, output, cache reads/writes, total tokens and Pi-reported pricing estimates have provenance. Context-window occupancy is not billed usage. No receipt means no usage assertion; absent cache or cost fields remain unknown. Pi calculates cost from its model catalog rates, so this is not an authoritative provider bill. |
@@ -224,7 +224,7 @@ initialization and the missing-credential admission path without a model call.
The exact reported `currentModelId` and billable receipt still need credentialed
local and Linux/Daytona qualification.
Profile version 2 declaration digest: `sha256:5ed73f923b24bf203cf39d6c5dedd5dfd21e94efcb2c2580b4adf53f1b91fd2f`. It hashes the versioned
Profile version 2 declaration digest: `sha256:c577a771778febe24b920388da77f6143971b8eec29e1ba9a433dac5b76de3cd`. It hashes the versioned
profile domain, patched wrapper source and platform closure pins. Every native
closure remains independently checked at launch. Version 1 warm sessions cannot
be reused with this integration.
@@ -170,7 +170,7 @@ impl AcpxProviderDescriptor {
"0.0.33",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"sha256:5ed73f923b24bf203cf39d6c5dedd5dfd21e94efcb2c2580b4adf53f1b91fd2f",
"sha256:c577a771778febe24b920388da77f6143971b8eec29e1ba9a433dac5b76de3cd",
),
"cursor" => (
self.model.as_str(),
@@ -2461,7 +2461,7 @@ mod tests {
"pi",
"pi-acp",
"0.0.33",
"sha256:5ed73f923b24bf203cf39d6c5dedd5dfd21e94efcb2c2580b4adf53f1b91fd2f",
"sha256:c577a771778febe24b920388da77f6143971b8eec29e1ba9a433dac5b76de3cd",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"openrouter/deepseek/deepseek-v4-flash-0731",
@@ -6,7 +6,7 @@
* three pins. Never accept a digest supplied only by an installed manifest.
*/
export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
"darwin-arm64": "c79fa33fd134e860fd777cd467873438b17b80b0b462a89e95cc63fbaa46bada",
"darwin-x64": "73624d1f59d55f7654bbe014ca30b55f5d0b3fbb3343467e71105414743a5c1c",
"linux-x64": "2f8ae83b42b12e6d4a99e347c245c274f67a2b180ab411441e8fe747fa84d72d",
"darwin-arm64": "a26e98fe3f4d3d93c13beffdb4e8b778d438dd1b3bca8a9b1610154342382681",
"darwin-x64": "9423812469c8aedba369c581b23a4302e4d86aac06e98e3d14d95d5e875597f0",
"linux-x64": "892085f1f22c0813e4d722c37ddaa343a01aecfeff360d8ca6d5b201dd3f01f5",
});
@@ -27,15 +27,48 @@ function harness() {
}
describe("owned Pi runtime extension", () => {
it("requires explicit isolated configuration and loopback authentication", () => {
it("requires explicit assigned configuration, authenticated HTTPS or numeric loopback HTTP", () => {
expect(() => readPiRuntimeConfiguration({})).toThrow("missing");
const value = { workspace: "/work/project", readOnly: false, readRoots: [], protectedRoots: [], instructions: "", servers: [{ type: "http", name: "paperclip", url: "http://127.0.0.1:1234/mcp", headers: [{ name: "Authorization", value: "Bearer 1234567890123456" }] }] };
const parse = () => readPiRuntimeConfiguration({ PAPERCLIP_PI_RUNTIME_CONFIGURATION: JSON.stringify(value) });
expect(parse().servers).toHaveLength(1);
value.servers[0]!.url = "https://127.0.0.1.evil.test/mcp";
expect(parse).toThrow("loopback");
value.servers[0]!.url = "http://user:password@127.0.0.1/mcp";
expect(parse).toThrow("loopback");
for (const url of ["http://127.0.0.1.evil.test/mcp", "http://paperclip.example/mcp", "http://localhost/mcp", "http://user:password@127.0.0.1/mcp", "https://user:password@paperclip.example/mcp", "https://paperclip.example/mcp#unbound", "file:///tmp/mcp"]) {
value.servers[0]!.url = url;
expect(parse).toThrow("requires HTTPS or numeric loopback HTTP");
}
value.servers[0]!.url = "https://paperclip.example/mcp/gateway";
value.servers[0]!.headers = [];
expect(parse).toThrow("authentication");
});
it("initializes and executes only the exact assigned HTTPS gateway with its bound credential", async () => {
const { config } = await workspace(); const h = harness();
const endpoint = "https://paperclip.example/api/mcp/gateway/assigned";
const authorization = "Bearer 1234567890123456";
const assigned = readPiRuntimeConfiguration({ PAPERCLIP_PI_RUNTIME_CONFIGURATION: JSON.stringify({
...config, servers: [{ type: "http", name: "paperclip-assigned", url: endpoint, headers: [{ name: "Authorization", value: authorization }] }],
}) });
const fetch_ = vi.fn<typeof fetch>(async (_url, init) => {
const body = JSON.parse(String(init!.body));
const result = body.method === "tools/list"
? { tools: [{ name: "report_progress", description: "Report progress", inputSchema: { type: "object", properties: {} } }] }
: body.method === "tools/call" ? { content: [{ type: "text", text: "recorded" }] } : {};
return new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }));
});
await installPiRuntimeExtension(h.api, assigned, (server, method, params, id, signal) => piMcpRequest(server, method, params, id, signal, fetch_));
expect(h.tools).toHaveLength(1);
expect(h.tools[0]!.name).toBe("mcp__paperclip-assigned__report_progress");
const signal = new AbortController().signal;
await expect(h.tools[0]!.execute("assigned-call-1", { text: "done" }, signal)).resolves.toMatchObject({ content: [{ text: "recorded" }] });
expect(fetch_.mock.calls.map(([, init]) => JSON.parse(String(init!.body)).method)).toEqual(["initialize", "tools/list", "tools/call"]);
for (const [url, init] of fetch_.mock.calls) {
expect(url).toBe(endpoint);
expect(init).toMatchObject({ method: "POST", redirect: "error", headers: { Authorization: authorization } });
}
expect(JSON.parse(String(fetch_.mock.calls.at(-1)![1]!.body))).toEqual({ jsonrpc: "2.0", id: "assigned-call-1", method: "tools/call", params: { name: "report_progress", arguments: { text: "done" } } });
const context = { cwd: config.workspace, ui: { select: vi.fn() } };
expect(await h.handlers.get("tool_call")!({ toolName: "mcp__unassigned__report_progress", toolCallId: "unassigned", input: {} }, context)).toMatchObject({ block: true });
expect(fetch_).toHaveBeenCalledTimes(3);
});
it("denies escapes and read-only mutations before requesting provider permission", async () => {
@@ -79,8 +79,11 @@ export function readPiRuntimeConfiguration(environment: NodeJS.ProcessEnv): PiRu
names.add(server.name);
if (typeof server.url !== "string") throw new Error("Pi MCP endpoint is invalid");
const url = new URL(server.url);
if (!['http:', 'https:'].includes(url.protocol) || !['127.0.0.1', '[::1]'].includes(url.hostname) || url.username || url.password || url.hash) {
throw new Error("Pi MCP endpoint must be a runner-owned loopback endpoint");
// Only session-bound gateways reach this configuration. Match the host's
// assigned transport policy: HTTPS remotely, cleartext only on loopback.
const cleartextLoopback = url.protocol === "http:" && ["127.0.0.1", "[::1]"].includes(url.hostname);
if ((url.protocol !== "https:" && !cleartextLoopback) || url.username || url.password || url.hash) {
throw new Error("Pi assigned MCP endpoint requires HTTPS or numeric loopback HTTP");
}
if (!Array.isArray(server.headers) || server.headers.length !== 1) throw new Error("Pi MCP authentication is invalid");
const header = asRecord(server.headers[0]);
@@ -54,7 +54,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
agentRuntimePackage: "@earendil-works/pi-coding-agent",
agentRuntimeVersion: "0.84.2",
commandDigest:
"sha256:5ed73f923b24bf203cf39d6c5dedd5dfd21e94efcb2c2580b4adf53f1b91fd2f",
"sha256:c577a771778febe24b920388da77f6143971b8eec29e1ba9a433dac5b76de3cd",
qualificationModel: "openrouter/deepseek/deepseek-v4-flash-0731",
reportedModelId: "openrouter/deepseek/deepseek-v4-flash-0731",
permissionPolicy: "interactive",