mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
perf(runner): narrow current tool authority reads (#17587)
Select the current run fields used by Native tool authority without transferring unused saved profiles or output. Preserve full context, receipts, authorization predicates, and locked mutation reads. Verified with full build and typecheck, 2,358 affected tests across 20 suites, a counterfactual real PostgreSQL byte regression, 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
c4c4f7eecb
commit
35d5b0686a
3 files changed
+64
-2
No files matched your search
@@ -123,6 +123,12 @@ launch context does not travel with each policy or parent-report check. JSON
|
||||
types, task-key precedence, and company and agent identity checks are preserved.
|
||||
Each decision still reads current data; this is not an authorization cache.
|
||||
|
||||
Native tool authority lookups select current run identity, status, invocation,
|
||||
responsible user, context, and receipt data. Saved profiles and output excerpts
|
||||
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.
|
||||
|
||||
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
|
||||
authority once. Different task bindings must both pass. The batch is discarded
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
issues,
|
||||
} from "@paperclipai/db";
|
||||
import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
|
||||
import { createDatabaseResponseCapture, installDatabaseResponseCapture } from "@paperclipai/db/response-capture";
|
||||
import { initializeRunIdentity, reserveSteeredIdentity, reconcileSteeredIdentity } from "../run-identity.js";
|
||||
import { documentService } from "../documents.js";
|
||||
import { issueService } from "../issues.js";
|
||||
@@ -198,6 +199,51 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
).rejects.toThrow("paperclip_runner_tool_not_advertised");
|
||||
});
|
||||
|
||||
it("reads current tool authority without transferring saved profiles or output", async () => {
|
||||
const binding = { companyId, agentId, issueId, runId };
|
||||
const authority = new PaperclipRunnerToolAuthority(db, binding);
|
||||
const contextCall = { tool: "get_task_context", callId: "context-read-width", arguments: {} };
|
||||
const apiCall = { tool: "search_api", callId: "api-read-width", arguments: { query: "read task" } };
|
||||
const expectedContext = await authority.execute(contextCall);
|
||||
const expectedApi = await authority.execute(apiCall);
|
||||
const [original] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
||||
const [actor] = await db.select({ status: agents.status }).from(agents).where(eq(agents.id, agentId));
|
||||
const payload = "saved-profile-and-output🙂".repeat(32_768);
|
||||
await db.update(heartbeatRuns).set({
|
||||
runnerProfileJson: { ...original.runnerProfileJson, retainedPayload: payload },
|
||||
stdoutExcerpt: payload,
|
||||
stderrExcerpt: payload,
|
||||
}).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();
|
||||
const capture = createDatabaseResponseCapture({ start: now - 100, end: now + 59_000 }, event => events.push(event));
|
||||
installDatabaseResponseCapture(observed, { heartbeatRuns, issues, agents, companies }, capture);
|
||||
try {
|
||||
const reader = new PaperclipRunnerToolAuthority(observed, binding);
|
||||
const context = await reader.execute(contextCall), api = await reader.execute(apiCall);
|
||||
expect(context).toEqual(expectedContext);
|
||||
expect(api).toEqual(expectedApi);
|
||||
expect(JSON.stringify([context, api])).not.toContain("saved-profile-and-output");
|
||||
capture.flush();
|
||||
const reads = events.filter(event => event.table === "heartbeat_runs" && event.operation === "select");
|
||||
expect(reads.length).toBeGreaterThan(0);
|
||||
expect(reads.reduce((bytes, event) => bytes + Number(event.responseValueBytes), 0)).toBeLessThan(16 * 1024);
|
||||
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)))[0]).toEqual(expanded);
|
||||
await db.update(agents).set({ status: "paused" }).where(eq(agents.id, agentId));
|
||||
await expect(reader.execute(contextCall)).rejects.toThrow("paperclip_runner_tool_binding_not_authorized");
|
||||
await expect(reader.execute(apiCall)).rejects.toThrow("paperclip_runner_tool_binding_not_authorized");
|
||||
} finally {
|
||||
capture.flush();
|
||||
await observed.$client.end();
|
||||
await db.update(agents).set({ status: actor.status }).where(eq(agents.id, agentId));
|
||||
await db.update(heartbeatRuns).set({
|
||||
runnerProfileJson: original.runnerProfileJson,
|
||||
stdoutExcerpt: original.stdoutExcerpt,
|
||||
stderrExcerpt: original.stderrExcerpt,
|
||||
}).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());
|
||||
|
||||
@@ -1227,7 +1227,17 @@ export class PaperclipRunnerToolAuthority {
|
||||
|
||||
async #boundContext() {
|
||||
if (this.binding.museRuntime && !this.binding.externalAuthorizingUserId) throw forbidden("Muse authorizer is unavailable");
|
||||
const [row] = await this.db.select({ issue: issues, actor: agents, run: heartbeatRuns })
|
||||
const [row] = await this.db.select({ issue: issues, actor: agents, run: {
|
||||
id: heartbeatRuns.id,
|
||||
companyId: heartbeatRuns.companyId,
|
||||
agentId: heartbeatRuns.agentId,
|
||||
runtimeMode: heartbeatRuns.runtimeMode,
|
||||
status: heartbeatRuns.status,
|
||||
invocationSource: heartbeatRuns.invocationSource,
|
||||
responsibleUserId: heartbeatRuns.responsibleUserId,
|
||||
contextSnapshot: heartbeatRuns.contextSnapshot,
|
||||
resultJson: heartbeatRuns.resultJson,
|
||||
} })
|
||||
.from(heartbeatRuns)
|
||||
.innerJoin(issues, eq(issues.id, this.binding.issueId))
|
||||
.innerJoin(agents, eq(agents.id, this.binding.agentId))
|
||||
@@ -2168,7 +2178,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
});
|
||||
}
|
||||
|
||||
async #sourcePlanApproval(context: { issue: typeof issues.$inferSelect; run: typeof heartbeatRuns.$inferSelect }) {
|
||||
async #sourcePlanApproval(context: { issue: typeof issues.$inferSelect; run: Pick<typeof heartbeatRuns.$inferSelect, "responsibleUserId"> }) {
|
||||
const plan = await handoffPlanContext(this.db, context.issue, this.#privacyActor(context.run));
|
||||
if (!plan || !this.binding.museRuntime) return plan;
|
||||
const document = await documentService(this.db).getIssueDocumentByKey(plan.sourceIssueId, "plan");
|
||||
|
||||
Reference in new issue
Block a user