mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(timeline): filter foreign and missing agent actors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e6c32a1251
commit
77954ba7a7
2 files changed
+36
-2
No files matched your search
@@ -201,4 +201,34 @@ export function registerTimelineReceiptBoundaryCases(f: Fixture) {
|
||||
expect(response.body.events.every((row: any) => actorIds.has(row.actorId))).toBe(true);
|
||||
expect(response.body.edges.every((row: any) => actorIds.has(row.fromActorId) && actorIds.has(row.toActorId))).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["foreign-actor", "missing-actor", "foreign-target", "missing-target", "resolver"] as const)("does not expose out-of-bound agent identity through %s", async kind => {
|
||||
const db = f.getDb(); const { companyId, userId, agentAId, agentBId } = await f.seedBase();
|
||||
const foreignCompanyId = randomUUID(), privateAgentId = randomUUID(), id = randomUUID(), at = new Date("2026-03-01T11:00:00Z");
|
||||
await db.insert(companies).values({ id: foreignCompanyId, name: "Foreign agent company", issuePrefix: "FA" });
|
||||
if (!kind.startsWith("missing")) await db.insert(agents).values({ id: privateAgentId, companyId: foreignCompanyId, name: "Foreign private agent", adapterType: "codex_local", status: "paused" });
|
||||
await db.insert(issues).values({ id, companyId, title: "Scoped agent facts", status: "done", createdByAgentId: kind === "resolver" ? privateAgentId : agentAId, createdAt: at });
|
||||
if (kind === "resolver") {
|
||||
await db.insert(issueComments).values({ companyId, issueId: id, authorAgentId: privateAgentId, body: "Recorded comment", createdAt: at });
|
||||
await db.insert(heartbeatRuns).values({ companyId, agentId: privateAgentId, status: "succeeded", invocationSource: "assignment", contextSnapshot: { issueId: id }, createdAt: at, startedAt: at, finishedAt: at });
|
||||
} else {
|
||||
await db.insert(activityLog).values({ companyId, actorType: "agent", actorId: kind.endsWith("actor") ? privateAgentId : agentAId,
|
||||
action: "issue.updated", entityType: "issue", entityId: id, createdAt: at,
|
||||
details: { assigneeAgentId: kind.endsWith("target") ? privateAgentId : agentBId } });
|
||||
}
|
||||
// Valid company-local assignment/run remains visible beside rejected facts.
|
||||
await db.insert(activityLog).values({ companyId, actorType: "agent", actorId: agentAId, action: "issue.updated", entityType: "issue", entityId: id, createdAt: at, details: { assigneeAgentId: agentBId } });
|
||||
const [validRun] = await db.insert(heartbeatRuns).values({ companyId, agentId: agentBId, status: "succeeded", invocationSource: "assignment", contextSnapshot: { issueId: id }, createdAt: at, startedAt: at, finishedAt: at }).returning();
|
||||
const response = await request(f.createApp(userId)).get(`/api/companies/${companyId}/timeline`).query({ from: "2026-03-01T00:00:00Z", to: "2026-03-02T00:00:00Z" });
|
||||
expect(response.status, JSON.stringify(response.body)).toBe(200);
|
||||
const json = JSON.stringify(response.body);
|
||||
for (const secret of [privateAgentId, "Foreign private agent"]) expect(json).not.toContain(secret);
|
||||
expect(response.body.edges).toContainEqual(expect.objectContaining({ fromActorId: `agent:${agentAId}`, toActorId: `agent:${agentBId}`, kind: "assignment" }));
|
||||
expect(response.body.spans).toContainEqual(expect.objectContaining({ runId: validRun!.id, actorId: `agent:${agentBId}` }));
|
||||
const actorIds = new Set(response.body.actors.map((row: any) => row.id));
|
||||
expect(response.body.events.every((row: any) => actorIds.has(row.actorId))).toBe(true);
|
||||
expect(response.body.spans.every((row: any) => actorIds.has(row.actorId))).toBe(true);
|
||||
expect(response.body.edges.every((row: any) => actorIds.has(row.fromActorId) && actorIds.has(row.toActorId))).toBe(true);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -796,7 +796,11 @@ export function workTimelineService(db: Db) {
|
||||
}
|
||||
|
||||
const actorMaps = await loadActorMaps(input.companyId, actorIds);
|
||||
const publicActorIds = new Set(Array.from(actorIds).filter(id => !id.startsWith("user:") || actorMaps.users.has(id.slice("user:".length))));
|
||||
const publicActorIds = new Set(Array.from(actorIds).filter(id => {
|
||||
if (id.startsWith("agent:")) return actorMaps.agents.has(id.slice("agent:".length));
|
||||
if (id.startsWith("user:")) return actorMaps.users.has(id.slice("user:".length));
|
||||
return true;
|
||||
}));
|
||||
const actors: WorkTimelineActor[] = Array.from(publicActorIds).map((id) => {
|
||||
const [type, rawId] = id.split(":", 2) as [TimelineActorType, string];
|
||||
if (type === "agent") {
|
||||
@@ -813,7 +817,7 @@ export function workTimelineService(db: Db) {
|
||||
|
||||
return {
|
||||
actors,
|
||||
spans: Array.from(spanByRunId.values()).sort((left, right) => left.start.localeCompare(right.start)),
|
||||
spans: Array.from(spanByRunId.values()).filter(span => publicActorIds.has(span.actorId)).sort((left, right) => left.start.localeCompare(right.start)),
|
||||
events: events.filter(event => publicActorIds.has(event.actorId)).sort((left, right) => left.at.localeCompare(right.at)),
|
||||
edges: edges.filter(edge => publicActorIds.has(edge.fromActorId) && publicActorIds.has(edge.toActorId)).sort((left, right) => left.at.localeCompare(right.at)),
|
||||
pagination: {
|
||||
|
||||
Reference in new issue
Block a user