perf(auth): project only required run context fields (#16380)

Project only the required JSON fields for saved-run policy and direct-parent report authorization. Preserve current reads, JSON types, identity checks, permission decisions and legacy task-key precedence.

Validation: 186 affected cases; 17 database transfer-budget cases after the shared startup-timeout correction; full typecheck/build; exact-head full CI, E2E, Superagent and Apex 5/5.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-14 22:45:08 -05:00
1 parent 1d5e2c89f0
commit 66724b8ce0
3 files changed
+107 -2

No files matched your search

+7
View File
@@ -36,6 +36,13 @@ secret redaction still run on the bounded result set.
Agent detail links fetch an older selected run by ID when it is outside the
history page, and verify that it belongs to the current company and agent.
Saved-run policy and parent-report lookups read only the `executionPolicy`
value or the legacy `issueId` and `taskId` values from a run context. PostgreSQL projects
those JSON values before it sends the row to the controller. Unused prompt and
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.
To compare the real summary and full history queries with an earlier revision:
```sh
@@ -0,0 +1,98 @@
import { randomUUID } from 'node:crypto';
import { eq, sql } from 'drizzle-orm';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { agents, companies, companyMemberships, createDb, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, heartbeatRuns, issues, projects, type Db } from '@paperclipai/db';
import { LOW_TRUST_REVIEW_PRESET } from '@paperclipai/shared';
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from './helpers/embedded-postgres.js';
import { authorizationService } from '../services/authorization.js';
const support = await getEmbeddedPostgresTestSupport();
(support.supported ? describe : describe.skip)('authorization run context transfer', () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let client: ReturnType<typeof createDb>['$client'];
let db: Db;
let recording = false;
let jsonBytes: number[] = [];
beforeAll(async () => {
database = await startEmbeddedPostgresTestDatabase('paperclip-authorization-context-');
db = createDb(database.connectionString, { maxConnections: 1 });
client = db.$client;
client.options.transform.value.from = (value, column) => {
if (recording && column.type === 3802 && value !== null) jsonBytes.push(Buffer.byteLength(JSON.stringify(value)));
return value;
};
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
beforeEach(() => { recording = false; jsonBytes = []; });
afterAll(async () => { await client?.end({ timeout: 5 }); await database?.cleanup(); });
async function seed() {
const companyId = randomUUID(), agentId = randomUUID(), projectId = randomUUID(), runId = randomUUID();
await db.insert(companies).values({ id: companyId, name: 'Context transfer', issuePrefix: `CT${companyId.slice(0, 6)}` });
await db.insert(agents).values({ id: agentId, companyId, name: 'Reader', role: 'engineer', adapterType: 'process' });
await db.insert(projects).values({ id: projectId, companyId, name: 'Allowed project' });
await db.insert(companyMemberships).values({ companyId, principalType: 'agent', principalId: agentId, status: 'active', membershipRole: 'member' });
const actor = { type: 'agent' as const, source: 'agent_jwt' as const, companyId, agentId, runId };
return { companyId, agentId, projectId, runId, actor };
}
it('retains saved run restrictions without transferring unrelated context', async () => {
const fixture = await seed();
await db.insert(heartbeatRuns).values({ id: fixture.runId, companyId: fixture.companyId, agentId: fixture.agentId,
contextSnapshot: { contextPadding: 'unrelated'.repeat(32_768), executionPolicy: {
trustPreset: LOW_TRUST_REVIEW_PRESET,
authorizationPolicy: { trustBoundary: { mode: LOW_TRUST_REVIEW_PRESET, projectIds: [fixture.projectId] } },
} } });
recording = true;
expect(await authorizationService(db).projectDiscoveryCandidateIds(fixture.actor, fixture.companyId)).toEqual([fixture.projectId]);
expect(jsonBytes.length).toBeGreaterThan(0);
expect(Math.max(...jsonBytes)).toBeLessThan(1024);
});
it.each([undefined, null, [], 'invalid', 42, true, {}])('keeps the standard fallback for saved policy %j', async executionPolicy => {
const fixture = await seed();
await db.insert(heartbeatRuns).values({ id: fixture.runId, companyId: fixture.companyId, agentId: fixture.agentId,
contextSnapshot: { contextPadding: 'unrelated'.repeat(32_768), executionPolicy } });
recording = true;
expect(await authorizationService(db).projectDiscoveryCandidateIds(fixture.actor, fixture.companyId)).toBeNull();
expect(jsonBytes.length).toBeGreaterThan(0);
expect(Math.max(...jsonBytes)).toBeLessThan(1024);
});
it.each([null, [], 'legacy', 42, true])('keeps the standard fallback for context container %j', async context => {
const fixture = await seed();
await db.insert(heartbeatRuns).values({ id: fixture.runId, companyId: fixture.companyId, agentId: fixture.agentId });
await db.execute(sql`update ${heartbeatRuns} set context_snapshot=${JSON.stringify(context)}::jsonb
where ${heartbeatRuns.id}=${fixture.runId} and ${heartbeatRuns.companyId}=${fixture.companyId}`);
expect(await authorizationService(db).projectDiscoveryCandidateIds(fixture.actor, fixture.companyId)).toBeNull();
});
it.each(['company', 'agent'])('does not take policy from another %s identity', async mismatch => {
const fixture = await seed(), other = await seed();
if (mismatch === 'agent') await db.update(agents).set({ companyId: fixture.companyId }).where(eq(agents.id, other.agentId));
await db.insert(heartbeatRuns).values({ id: other.runId,
companyId: mismatch === 'company' ? other.companyId : fixture.companyId, agentId: other.agentId,
contextSnapshot: { contextPadding: 'unrelated'.repeat(32_768), executionPolicy: {
trustPreset: LOW_TRUST_REVIEW_PRESET,
authorizationPolicy: { trustBoundary: { mode: LOW_TRUST_REVIEW_PRESET, projectIds: [other.projectId] } },
} } });
recording = true;
expect(await authorizationService(db).projectDiscoveryCandidateIds({ ...fixture.actor, runId: other.runId }, fixture.companyId)).toBeNull();
expect(Math.max(...jsonBytes)).toBeLessThan(1024);
});
it.each(['issueId', 'taskId'])('retains the direct-parent report for legacy context key %s', async key => {
const fixture = await seed(), parentId = randomUUID(), childId = randomUUID();
await db.insert(issues).values({ id: parentId, companyId: fixture.companyId, projectId: fixture.projectId, title: 'Parent' });
await db.insert(issues).values({ id: childId, companyId: fixture.companyId, projectId: fixture.projectId, parentId,
assigneeAgentId: fixture.agentId, title: 'Child' });
await db.insert(heartbeatRuns).values({ id: fixture.runId, companyId: fixture.companyId, agentId: fixture.agentId,
contextSnapshot: { [key]: childId, contextPadding: 'unrelated'.repeat(32_768) } });
await db.update(issues).set({ checkoutRunId: fixture.runId }).where(eq(issues.id, childId));
recording = true;
const decision = await authorizationService(db).decide({ actor: fixture.actor, action: 'issue:comment',
resource: { type: 'issue', companyId: fixture.companyId, projectId: fixture.projectId, issueId: parentId } });
expect(decision).toMatchObject({ allowed: true, reason: 'allow_direct_parent_report' });
expect(jsonBytes.length).toBeGreaterThan(0);
expect(Math.max(...jsonBytes)).toBeLessThan(1024);
});
});
+2 -2
View File
@@ -974,7 +974,7 @@ export function authorizationService(db: Db | DbTransaction) {
id: heartbeatRuns.id,
companyId: heartbeatRuns.companyId,
agentId: heartbeatRuns.agentId,
contextSnapshot: heartbeatRuns.contextSnapshot,
contextSnapshot: sql<Record<string, unknown>>`jsonb_build_object('executionPolicy', ${heartbeatRuns.contextSnapshot}->'executionPolicy')`,
})
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, runId))
@@ -992,7 +992,7 @@ export function authorizationService(db: Db | DbTransaction) {
.select({
companyId: heartbeatRuns.companyId,
agentId: heartbeatRuns.agentId,
contextSnapshot: heartbeatRuns.contextSnapshot,
contextSnapshot: sql<Record<string, unknown>>`jsonb_build_object('issueId', ${heartbeatRuns.contextSnapshot}->'issueId', 'taskId', ${heartbeatRuns.contextSnapshot}->'taskId')`,
})
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, runId))