mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
perf(runner): skip wake payloads in ordinary authority reads (#17600)
Omit saved wake payloads from ordinary unlocked Native authority reads. Load the current full wake for mail restrictions. Preserve stored rows, locked mutation reads, and current authorization predicates. Verified with full build and typecheck, 2,360 affected tests, real PostgreSQL byte and response checks, counterfactual performance and mail authorization failures, complete exact-head CI, and Greptile 5/5 Medium with no unresolved comments. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2e47c42fad
commit
8c6bd5a83b
3 files changed
+50
-8
No files matched your search
+4
-2
@@ -129,8 +129,10 @@ stay in storage and are omitted from that read. Task and agent binding, current
|
||||
status, budget, and task privacy checks retain their existing predicates.
|
||||
Mutation transactions still lock and read complete run records. The unlocked
|
||||
authority read also omits named launch renderings and preparation data that its
|
||||
tools do not consume. Full wake payloads, other context, and result receipts
|
||||
remain available; the complete saved context stays in storage for execution.
|
||||
tools do not consume. Ordinary tool checks also omit the wake payload. Mail
|
||||
checks load the current full wake to enforce external-chat restrictions. Other
|
||||
context and result receipts remain available. The complete saved context stays
|
||||
in storage for execution.
|
||||
|
||||
WebSocket delivery uses one fresh authorization batch for an event's task and
|
||||
run bindings. When both bindings name the same task, the batch reads its current
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { connectorToolDefinitionsForSkills } from "../connector-runtime.js";
|
||||
import * as connectorRuntime from "../connector-runtime.js";
|
||||
import * as cloudIdentity from "../cloud-runtime-identity.js";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -101,7 +101,7 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
});
|
||||
|
||||
it("registers pinned connector definitions without discovery and rejects revoked assignments at use", async () => {
|
||||
const connectorAssignments = connectorToolDefinitionsForSkills(new Set([
|
||||
const connectorAssignments = connectorRuntime.connectorToolDefinitionsForSkills(new Set([
|
||||
"paperclipai/paperclip/slack", "paperclipai/paperclip/browser-use-cloud", "custom/agentmail",
|
||||
]));
|
||||
expect(connectorAssignments).toHaveLength(1);
|
||||
@@ -244,7 +244,7 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reads tool authority without transferring unused launch renderings", async () => {
|
||||
it("reads tool authority without transferring unused launch renderings or wake payloads", async () => {
|
||||
const binding = { companyId, agentId, issueId, runId };
|
||||
const authority = new PaperclipRunnerToolAuthority(db, binding);
|
||||
const contextCall = { tool: "get_task_context", callId: "launch-context-read", arguments: {} };
|
||||
@@ -255,7 +255,7 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
const context = { ...original.contextSnapshot, executionContinuation: { savedInput: payload }, paperclipTaskMarkdown: payload,
|
||||
paperclipTaskMarkdownCompact: payload, paperclipTaskMarkdownAssignment: payload, paperclipTaskMarkdownAssignmentCompact: payload,
|
||||
paperclipWorkspace: { savedInput: payload }, paperclipEnvironment: { savedInput: payload },
|
||||
paperclipContinuationSummary: payload, paperclipWakeComment: payload };
|
||||
paperclipContinuationSummary: payload, paperclipWakeComment: payload, paperclipWake: { retainedPayload: payload } };
|
||||
await db.update(heartbeatRuns).set({ contextSnapshot: context }).where(eq(heartbeatRuns.id, runId));
|
||||
const [expanded] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
||||
const observed = createDb(temporary!.connectionString), events: Array<Record<string, unknown>> = [], now = Date.now();
|
||||
@@ -280,6 +280,45 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("checks the current full wake before mail actions while ordinary tools omit its payload", async () => {
|
||||
const connectorAssignments = connectorRuntime.connectorToolDefinitionsForSkills(new Set(["paperclipai/paperclip/agentmail"]));
|
||||
const name = connectorAssignments.flatMap(assignment => assignment.tools).find(tool => tool.name.startsWith("agentmail_"))!.name;
|
||||
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId, connectorAssignments });
|
||||
const [original] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
||||
const dispatch = vi.spyOn(connectorRuntime, "executeConnectorTool").mockResolvedValue({ outcome: "mail-dispatched" });
|
||||
const wake = {
|
||||
reason: "External chat message received", externalChatProvider: "github", checkedOutByHarness: true,
|
||||
issue: { id: issueId, identifier: "RNT-1", title: "Conversation", description: "Task", descriptionTruncated: false,
|
||||
status: "in_progress", workMode: "standard" },
|
||||
commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0 }, commentIds: ["wake-comment"],
|
||||
latestCommentId: "wake-comment", comments: [{ id: "wake-comment", issueId, body: "Reply here", bodyTruncated: false, authorType: "user" }],
|
||||
fallbackFetchNeeded: false,
|
||||
};
|
||||
const call = { tool: name, callId: "current-mail-wake", arguments: {} };
|
||||
try {
|
||||
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, paperclipWake: wake } }).where(eq(heartbeatRuns.id, runId));
|
||||
await expect(authority.execute(call)).rejects.toThrow("Restricted chat runs cannot use email actions");
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
await expect(authority.execute({ tool: "get_task_context", callId: "chat-task-context", arguments: {} }))
|
||||
.resolves.toMatchObject({ activeTask: { id: issueId } });
|
||||
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, runId));
|
||||
await expect(authority.execute(call)).resolves.toEqual({ outcome: "mail-dispatched" });
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, paperclipWake: { ...wake, checkedOutByHarness: false,
|
||||
externalChatExecutionBound: true, issue: { ...wake.issue, status: "in_review" } } } }).where(eq(heartbeatRuns.id, runId));
|
||||
await expect(authority.execute(call)).rejects.toThrow("Restricted chat runs cannot use email actions");
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
for (const context of [{ issueId, source: "chat:github" }, { issueId, paperclipExternalChatQuestionResponse: true }]) {
|
||||
await db.update(heartbeatRuns).set({ contextSnapshot: context }).where(eq(heartbeatRuns.id, runId));
|
||||
await expect(authority.execute(call)).rejects.toThrow("Restricted chat runs cannot use email actions");
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
} finally {
|
||||
dispatch.mockRestore();
|
||||
await db.update(heartbeatRuns).set({ contextSnapshot: original.contextSnapshot }).where(eq(heartbeatRuns.id, runId));
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves platform receipt provenance in both mixed task history readers", async () => {
|
||||
const at = new Date("2026-10-10T12:00:00Z");
|
||||
const commentIds = Array.from({ length: 4 }, () => randomUUID());
|
||||
|
||||
@@ -449,7 +449,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
}
|
||||
if (isConnectorTool(call.tool)) {
|
||||
if (!(this.binding.connectorAssignments ?? []).some((assignment) => assignment.tools.some((tool) => tool.name === call.tool))) throw forbidden("Connector tool is not available to this run");
|
||||
const { run } = await this.#boundContext();
|
||||
const { run } = await this.#boundContext({ includeChatWake: call.tool.startsWith("agentmail_") });
|
||||
const snapshot = record(run.contextSnapshot);
|
||||
if (call.tool.startsWith("agentmail_") && (isPaperclipExternalChatContractTurn(snapshot.paperclipWake) || String(snapshot.source ?? "").startsWith("chat:") || snapshot.paperclipExternalChatQuestionResponse)) throw forbidden("Restricted chat runs cannot use email actions");
|
||||
return executeConnectorTool(this.db, this.binding, call.tool, call.arguments);
|
||||
@@ -1225,7 +1225,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
...(this.binding.museRuntime ? { authorizingUserId: this.binding.externalAuthorizingUserId } : {}) };
|
||||
}
|
||||
|
||||
async #boundContext() {
|
||||
async #boundContext(options: { includeChatWake?: boolean } = {}) {
|
||||
if (this.binding.museRuntime && !this.binding.externalAuthorizingUserId) throw forbidden("Muse authorizer is unavailable");
|
||||
const [row] = await this.db.select({ issue: issues, actor: agents, run: {
|
||||
id: heartbeatRuns.id,
|
||||
@@ -1239,6 +1239,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
then ${heartbeatRuns.contextSnapshot} - array['executionContinuation', 'paperclipTaskMarkdown',
|
||||
'paperclipTaskMarkdownCompact', 'paperclipTaskMarkdownAssignment', 'paperclipTaskMarkdownAssignmentCompact',
|
||||
'paperclipWorkspace', 'paperclipEnvironment', 'paperclipContinuationSummary', 'paperclipWakeComment']
|
||||
${options.includeChatWake ? sql`` : sql`- 'paperclipWake'`}
|
||||
else ${heartbeatRuns.contextSnapshot} end`,
|
||||
resultJson: heartbeatRuns.resultJson,
|
||||
} })
|
||||
|
||||
Reference in new issue
Block a user