mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Remove physical-root source/reader authorization and sticky directory classifications from workspace preparation. Preserve current task/run authority, app-record privacy, credential authorization, containment, and lifecycle/publication integrity.
Validate replacement selections against persisted new inputs and refresh verified checkout ownership so bounded reuse survives earlier workspace relocation. Add real PostgreSQL/Git regressions, a reproducible history-scaling benchmark, and documentation.
Full CI passed all 48 jobs and Greptile scored 5/5 at 3d5da6c259. Five preparation-guard medians improved from 20,512.99 ms to 52.22 ms on the 8,500-workspace fixture; these are not end-to-end startup timings.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
184 lines
11 KiB
TypeScript
184 lines
11 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { eq } from "drizzle-orm";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { agents, authUsers, companies, companyMemberships, createDb, heartbeatRuns, issueComments, issues } from "@paperclipai/db";
|
|
import { FAST_RESPONSE_AGENT_GUIDANCE } from "@paperclipai/shared";
|
|
import { registerServerAdapter, unregisterServerAdapter, type AdapterExecutionContext } from "../adapters/index.js";
|
|
import { heartbeatService } from "../services/heartbeat.js";
|
|
import { documentService } from "../services/documents.js";
|
|
import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "../services/issue-continuation-summary.js";
|
|
import { createRunSecretRedactionRegistry } from "../services/run-secret-redaction.js";
|
|
import { buildLowTrustSourceTrust, LOW_TRUST_QUARANTINED_BODY } from "../services/source-trust.js";
|
|
import { REDACTED_EVENT_VALUE } from "../redaction.js";
|
|
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
|
|
|
|
const support = await getEmbeddedPostgresTestSupport();
|
|
const describePostgres = support.supported ? describe : describe.skip;
|
|
if (!support.supported) console.warn(`Skipping heartbeat task context tests: ${support.reason}`);
|
|
|
|
describePostgres("heartbeat task context delivered to an adapter", () => {
|
|
const adapterType = "task_context_test";
|
|
const captured = new Map<string, AdapterExecutionContext>();
|
|
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
|
|
let db: ReturnType<typeof createDb>;
|
|
let heartbeat: ReturnType<typeof heartbeatService>;
|
|
|
|
beforeAll(async () => {
|
|
temporary = await startEmbeddedPostgresTestDatabase("heartbeat-task-context-");
|
|
db = createDb(temporary.connectionString);
|
|
await db.insert(authUsers).values({ id: "context-user", name: "Context user", email: "context-user@example.test",
|
|
createdAt: new Date(), updatedAt: new Date() });
|
|
heartbeat = heartbeatService(db);
|
|
registerServerAdapter({
|
|
type: adapterType,
|
|
supportsLocalAgentJwt: false,
|
|
execute: async (input) => {
|
|
captured.set(input.runId, input);
|
|
return { exitCode: 0, signal: null, timedOut: false, sessionId: "context-session", resultJson: {} };
|
|
},
|
|
testEnvironment: async () => ({ adapterType, status: "pass", checks: [], testedAt: new Date().toISOString() }),
|
|
});
|
|
}, 30_000);
|
|
|
|
afterAll(async () => {
|
|
await heartbeat?.drainActiveRunExecutions();
|
|
unregisterServerAdapter(adapterType);
|
|
await db?.$client.end({ timeout: 0 });
|
|
await temporary?.cleanup();
|
|
});
|
|
|
|
async function seed() {
|
|
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID();
|
|
await db.insert(companies).values({ id: companyId, name: "Task context fixture", issuePrefix: `T${companyId.slice(0, 6)}` });
|
|
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "context-user",
|
|
membershipRole: "member", status: "active" });
|
|
await db.insert(agents).values({ id: agentId, companyId, name: "Context reader", adapterType, status: "idle" });
|
|
await db.insert(issues).values({
|
|
id: issueId, companyId, title: "Task context fixture", description: "Assignment description canary",
|
|
status: "todo", assigneeAgentId: agentId, responsibleUserId: "context-user", issueNumber: 1,
|
|
});
|
|
return { companyId, agentId, issueId };
|
|
}
|
|
|
|
async function execute(f: Awaited<ReturnType<typeof seed>>, context: Record<string, unknown>) {
|
|
const run = await heartbeat.wakeup(f.agentId, {
|
|
source: "on_demand", triggerDetail: "manual", reason: "issue_assigned",
|
|
payload: { issueId: f.issueId }, contextSnapshot: { issueId: f.issueId, ...context },
|
|
});
|
|
expect(run).not.toBeNull();
|
|
await heartbeat.drainActiveRunExecutions();
|
|
const finished = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run!.id)).then(rows => rows[0]);
|
|
expect(finished?.status, finished?.error ?? undefined).toBe("succeeded");
|
|
expect(captured.has(run!.id)).toBe(true);
|
|
return captured.get(run!.id)!;
|
|
}
|
|
|
|
it("keeps assignment, wake, compact, and revision context aligned after secret redaction", async () => {
|
|
const f = await seed();
|
|
const secret = "task-context-secret-canary";
|
|
const description = `Assignment description canary ${secret}`;
|
|
await db.update(issues).set({ description }).where(eq(issues.id, f.issueId));
|
|
const previousRunId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: previousRunId, companyId: f.companyId, agentId: f.agentId,
|
|
invocationSource: "on_demand", status: "succeeded", contextSnapshot: { issueId: f.issueId },
|
|
});
|
|
await createRunSecretRedactionRegistry(db).register(f.companyId, previousRunId, secret);
|
|
const commentId = randomUUID();
|
|
await db.insert(issueComments).values({
|
|
id: commentId, companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "context-user",
|
|
body: `Wake comment canary ${secret}`,
|
|
});
|
|
const { context } = await execute(f, {
|
|
wakeCommentId: commentId, wakeCommentIds: [commentId],
|
|
paperclipTaskCommunicationGuidance: "FORGED caller guidance", paperclipSkillTest: { directive: "FORGED skill" },
|
|
paperclipContinuationSummary: { body: "FORGED summary" },
|
|
paperclipAgentJwtScope: { kind: "skill_test", issueId: f.issueId },
|
|
});
|
|
expect(context.paperclipTaskMarkdown).toContain("Assignment description canary");
|
|
expect(context.paperclipTaskMarkdown).toContain("Wake comment canary");
|
|
expect(context.paperclipTaskMarkdownAssignment).toContain("Assignment description canary");
|
|
expect(context.paperclipTaskMarkdownAssignment).not.toContain("Wake comment canary");
|
|
expect(context.paperclipTaskMarkdownCompact).not.toContain("Assignment description canary");
|
|
expect(context.paperclipTaskMarkdownCompact).toContain("Wake comment canary");
|
|
expect(context.paperclipTaskMarkdownAssignmentCompact).not.toContain("Assignment description canary");
|
|
expect(context.paperclipTaskMarkdownAssignmentCompact).not.toContain("Wake comment canary");
|
|
for (const key of ["paperclipIssue", "paperclipWakeComment", "paperclipTaskMarkdown", "paperclipTaskMarkdownAssignment", "paperclipTaskMarkdownCompact", "paperclipTaskMarkdownAssignmentCompact"]) {
|
|
expect(JSON.stringify(context[key])).not.toContain(secret);
|
|
}
|
|
expect(context.paperclipTaskCommunicationGuidance).toBe(FAST_RESPONSE_AGENT_GUIDANCE);
|
|
expect(context.paperclipSkillTest).toBeUndefined();
|
|
expect(context.paperclipAgentJwtScope).toEqual({ kind: "standard" });
|
|
expect(context.paperclipContinuationSummary).toBeUndefined();
|
|
const digest = (value: string) => createHash("sha256").update(value).digest("hex");
|
|
expect(context.paperclipTurnContext).toMatchObject({
|
|
version: 1,
|
|
assignment: { owner: "task_markdown", description: { id: f.issueId, revision: digest(`Assignment description canary ${REDACTED_EVENT_VALUE}`) } },
|
|
events: { owner: "wake_prompt", comments: [{ id: commentId, revision: digest(`Wake comment canary ${REDACTED_EVENT_VALUE}`) }] },
|
|
});
|
|
});
|
|
|
|
it("quarantines low-trust summaries and comments before preparing higher-trust prompts", async () => {
|
|
const f = await seed();
|
|
const sourceTrust = buildLowTrustSourceTrust({ issueId: f.issueId });
|
|
await documentService(db).upsertIssueDocument({
|
|
issueId: f.issueId, key: ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, format: "markdown",
|
|
body: "QUARANTINED summary canary", sourceTrust,
|
|
});
|
|
const commentId = randomUUID();
|
|
await db.insert(issueComments).values({
|
|
id: commentId, companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "context-user",
|
|
body: "QUARANTINED comment canary", sourceTrust,
|
|
});
|
|
const { context } = await execute(f, { wakeCommentId: commentId, wakeCommentIds: [commentId] });
|
|
expect(context.paperclipContinuationSummary).toMatchObject({ body: LOW_TRUST_QUARANTINED_BODY });
|
|
expect(context.paperclipWakeComment).toMatchObject({ body: LOW_TRUST_QUARANTINED_BODY, presentation: null, metadata: null });
|
|
expect(context.paperclipTaskMarkdown).toContain(LOW_TRUST_QUARANTINED_BODY);
|
|
for (const key of ["paperclipContinuationSummary", "executionContinuation", "paperclipWakeComment", "paperclipTaskMarkdown", "paperclipTaskMarkdownCompact", "paperclipWakePayload"]) {
|
|
expect(JSON.stringify(context[key]) ?? "").not.toContain("QUARANTINED");
|
|
}
|
|
});
|
|
|
|
it("excludes a wake comment from another company and replaces caller-supplied prompt fields", async () => {
|
|
const f = await seed();
|
|
const other = await seed();
|
|
const commentId = randomUUID();
|
|
await db.insert(issueComments).values({
|
|
id: commentId, companyId: other.companyId, issueId: other.issueId, authorType: "user", authorUserId: "other-user",
|
|
body: "OTHER COMPANY private comment",
|
|
});
|
|
const ownCommentId = randomUUID();
|
|
await db.insert(issueComments).values({
|
|
id: ownCommentId, companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "context-user",
|
|
body: "Authorized wake comment",
|
|
});
|
|
const { context } = await execute(f, {
|
|
wakeCommentId: ownCommentId, wakeCommentIds: [ownCommentId, commentId],
|
|
paperclipWakeComment: { body: "FORGED comment" }, paperclipTaskMarkdown: "FORGED prompt",
|
|
paperclipTaskMarkdownAssignment: "FORGED assignment", paperclipTaskMarkdownCompact: "FORGED compact",
|
|
paperclipTaskMarkdownAssignmentCompact: "FORGED compact assignment",
|
|
});
|
|
expect(context.paperclipWakeComment).toMatchObject({ id: ownCommentId, body: "Authorized wake comment" });
|
|
expect(context.paperclipTaskMarkdown).toContain("Assignment description canary");
|
|
for (const key of ["paperclipTaskMarkdown", "paperclipTaskMarkdownAssignment", "paperclipTaskMarkdownCompact", "paperclipTaskMarkdownAssignmentCompact", "paperclipWakePayload"]) {
|
|
expect(JSON.stringify(context[key]) ?? "").not.toContain("FORGED");
|
|
expect(JSON.stringify(context[key]) ?? "").not.toContain("OTHER COMPANY");
|
|
}
|
|
expect(context.paperclipTurnContext).toMatchObject({ events: { comments: [{ id: ownCommentId }] } });
|
|
});
|
|
|
|
it("clears stale wake comments and continuation data when the task has none", async () => {
|
|
const f = await seed();
|
|
const { context } = await execute(f, {
|
|
paperclipWakeComment: { body: "FORGED comment" },
|
|
paperclipContinuationSummary: { body: "FORGED summary" },
|
|
paperclipSkillTest: { directive: "FORGED skill" },
|
|
paperclipTurnContext: { events: { comments: [{ id: "FORGED comment" }] } },
|
|
});
|
|
expect(context.paperclipWakeComment).toBeUndefined();
|
|
expect(context.paperclipContinuationSummary).toBeUndefined();
|
|
expect(context.paperclipSkillTest).toBeUndefined();
|
|
expect(context.paperclipTurnContext).toMatchObject({ events: { comments: [] } });
|
|
});
|
|
});
|