diff --git a/packages/db/src/migrations/0177_activity_log_responsible_user.sql b/packages/db/src/migrations/0177_activity_log_responsible_user.sql new file mode 100644 index 0000000000..20d9b27145 --- /dev/null +++ b/packages/db/src/migrations/0177_activity_log_responsible_user.sql @@ -0,0 +1,10 @@ +ALTER TABLE "activity_log" + ADD COLUMN IF NOT EXISTS "responsible_user_id" text; + +-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because this forward-only index is required for the new agent audit feed. +CREATE INDEX IF NOT EXISTS "activity_log_company_agent_created_idx" + ON "activity_log" USING btree ("company_id", "agent_id", "created_at"); + +-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations run transactionally, so CONCURRENTLY is unavailable because this forward-only index is required for the new responsible-user audit feed. +CREATE INDEX IF NOT EXISTS "activity_log_company_responsible_user_created_idx" + ON "activity_log" USING btree ("company_id", "responsible_user_id", "created_at"); diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 2e32c8fb8a..197e97dade 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1226,6 +1226,13 @@ "when": 1784211956161, "tag": "0176_issue_create_idempotency_key_expiry", "breakpoints": true + }, + { + "idx": 177, + "version": "7", + "when": 1784241826832, + "tag": "0177_activity_log_responsible_user", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/activity_log.ts b/packages/db/src/schema/activity_log.ts index 3381f9de04..5633c7b6ad 100644 --- a/packages/db/src/schema/activity_log.ts +++ b/packages/db/src/schema/activity_log.ts @@ -15,11 +15,22 @@ export const activityLog = pgTable( entityId: text("entity_id").notNull(), agentId: uuid("agent_id").references(() => agents.id), runId: uuid("run_id").references(() => heartbeatRuns.id), + responsibleUserId: text("responsible_user_id"), details: jsonb("details").$type>(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ companyCreatedIdx: index("activity_log_company_created_idx").on(table.companyId, table.createdAt), + companyAgentCreatedIdx: index("activity_log_company_agent_created_idx").on( + table.companyId, + table.agentId, + table.createdAt, + ), + companyResponsibleUserCreatedIdx: index("activity_log_company_responsible_user_created_idx").on( + table.companyId, + table.responsibleUserId, + table.createdAt, + ), runIdIdx: index("activity_log_run_id_idx").on(table.runId), entityIdx: index("activity_log_entity_type_id_idx").on(table.entityType, table.entityId), }), diff --git a/server/src/__tests__/activity-log-responsible-user.test.ts b/server/src/__tests__/activity-log-responsible-user.test.ts new file mode 100644 index 0000000000..d631ff15fb --- /dev/null +++ b/server/src/__tests__/activity-log-responsible-user.test.ts @@ -0,0 +1,234 @@ +import { randomUUID } from "node:crypto"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { eq } from "drizzle-orm"; +import { + activityLog, + agentApiKeys, + agents, + companies, + createDb, + heartbeatRuns, + issues, + type Db, +} from "@paperclipai/db"; +import { + logActivity, + resolveResponsibleUserIdForActivity, + type LogActivityInput, +} from "../services/activity-log.js"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; + +type TableRows = Map>>; + +const companyId = "00000000-0000-4000-8000-000000000001"; +const agentId = "00000000-0000-4000-8000-000000000002"; +const issueId = "00000000-0000-4000-8000-000000000003"; +const runId = "00000000-0000-4000-8000-000000000004"; +const missingRunId = "00000000-0000-4000-8000-000000000005"; +const agentApiKeyId = "00000000-0000-4000-8000-000000000006"; +const missingAgentApiKeyId = "00000000-0000-4000-8000-000000000007"; + +function createReader(rowsByTable: TableRows) { + return { + select: () => ({ + from: (table: unknown) => ({ + where: (condition: unknown) => { + expect(condition).toBeDefined(); + return Promise.resolve(rowsByTable.get(table) ?? []); + }, + }), + }), + } as unknown as Db; +} + +function activityInput(overrides: Partial = {}): LogActivityInput { + return { + companyId, + actorType: "agent", + actorId: agentId, + action: "issue.updated", + entityType: "issue", + entityId: issueId, + agentId, + ...overrides, + }; +} + +describe("resolveResponsibleUserIdForActivity", () => { + it("attributes user actions directly without database lookups", async () => { + const db = { + select: () => { + throw new Error("user attribution should not query the database"); + }, + } as unknown as Db; + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + actorType: "user", + actorId: "user-1", + entityType: "company", + entityId: companyId, + }))).resolves.toBe("user-1"); + }); + + it("prefers the heartbeat run responsible user", async () => { + const db = createReader(new Map([ + [heartbeatRuns, [{ responsibleUserId: "run-user" }]], + [issues, [{ responsibleUserId: "issue-user", createdByUserId: null }]], + [agentApiKeys, [{ responsibleUserId: "key-user" }]], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + runId, + agentApiKeyId, + }))).resolves.toBe("run-user"); + }); + + it("falls back to issue attribution when the run is unavailable", async () => { + const db = createReader(new Map([ + [heartbeatRuns, []], + [issues, [{ responsibleUserId: "issue-user", createdByUserId: "creator-user" }]], + [agentApiKeys, [{ responsibleUserId: "key-user" }]], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + runId: missingRunId, + agentApiKeyId, + }))).resolves.toBe("issue-user"); + }); + + it("uses explicit issue context for non-issue activity", async () => { + const db = createReader(new Map([ + [issues, [{ responsibleUserId: "issue-user", createdByUserId: null }]], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + entityType: "heartbeat_run", + entityId: runId, + issueId, + }))).resolves.toBe("issue-user"); + }); + + it("uses the active agent API key responsible user for no-run actions", async () => { + const db = createReader(new Map([ + [agentApiKeys, [{ responsibleUserId: "key-user" }]], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + entityType: "agent", + entityId: agentId, + agentApiKeyId, + }))).resolves.toBe("key-user"); + }); + + it("falls back to the company default responsible user", async () => { + const db = createReader(new Map([ + [agentApiKeys, []], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + entityType: "company", + entityId: companyId, + agentApiKeyId: missingAgentApiKeyId, + }))).resolves.toBe("default-user"); + }); + + it("uses issue creator attribution when responsibleUserId is absent", async () => { + const db = createReader(new Map([ + [issues, [{ responsibleUserId: null, createdByUserId: "creator-user" }]], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput())).resolves.toBe("creator-user"); + }); + + it("ignores malformed UUID-backed identifiers", async () => { + const db = createReader(new Map([ + [heartbeatRuns, [{ responsibleUserId: "run-user" }]], + [issues, [{ responsibleUserId: "issue-user", createdByUserId: null }]], + [agentApiKeys, [{ responsibleUserId: "key-user" }]], + [companies, [{ defaultResponsibleUserId: "default-user" }]], + ])); + + await expect(resolveResponsibleUserIdForActivity(db, activityInput({ + runId: "not-a-run-uuid", + entityId: "not-an-issue-uuid", + agentApiKeyId: "not-a-key-uuid", + details: { issueId }, + }))).resolves.toBe("default-user"); + }); +}); + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +describeEmbeddedPostgres("logActivity responsible-user stamping", () => { + let db!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-activity-responsible-user-"); + db = createDb(tempDb.connectionString); + }, 20_000); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + it("persists API-key attribution for an out-of-run action", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const agentApiKeyId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + defaultResponsibleUserId: "default-user", + requireBoardApprovalForNewAgents: false, + }); + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "running", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + await db.insert(agentApiKeys).values({ + id: agentApiKeyId, + companyId, + agentId, + name: "test", + keyHash: `hash-${agentApiKeyId}`, + responsibleUserId: "key-user", + }); + + await logActivity(db, activityInput({ + companyId, + actorId: agentId, + agentId, + entityType: "agent", + entityId: agentId, + agentApiKeyId, + })); + + const row = await db + .select({ responsibleUserId: activityLog.responsibleUserId }) + .from(activityLog) + .where(eq(activityLog.companyId, companyId)) + .then((rows) => rows[0]); + + expect(row?.responsibleUserId).toBe("key-user"); + }); +}); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 21e1970819..cff27b96c0 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -1948,6 +1948,7 @@ export function agentRoutes( entityId: updated.id, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, details: { adapterType: updated.adapterType, desiredSkills, @@ -2275,6 +2276,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.config_rolled_back", entityType: "agent", entityId: updated.id, @@ -2477,6 +2479,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.hire_created", entityType: "agent", entityId: agent.id, @@ -2507,6 +2510,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "approval.created", entityType: "approval", entityId: approval.id, @@ -2600,6 +2604,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.created", entityType: "agent", entityId: agent.id, @@ -2681,6 +2686,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.permissions_updated", entityType: "agent", entityId: agent.id, @@ -2756,6 +2762,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.instructions_path_updated", entityType: "agent", entityId: agent.id, @@ -2813,6 +2820,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.instructions_bundle_updated", entityType: "agent", entityId: existing.id, @@ -2875,6 +2883,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.instructions_file_updated", entityType: "agent", entityId: existing.id, @@ -2908,6 +2917,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.instructions_file_deleted", entityType: "agent", entityId: existing.id, @@ -3063,6 +3073,7 @@ export function agentRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "agent.updated", entityType: "agent", entityId: agent.id, diff --git a/server/src/routes/assets.ts b/server/src/routes/assets.ts index 6b43e39154..db8de4b030 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -183,6 +183,7 @@ export function assetRoutes(db: Db, storage: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "asset.created", entityType: "asset", entityId: asset.id, @@ -281,6 +282,7 @@ export function assetRoutes(db: Db, storage: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "asset.created", entityType: "asset", entityId: asset.id, diff --git a/server/src/routes/authz.ts b/server/src/routes/authz.ts index 634aa5f90d..f8ed72d7ee 100644 --- a/server/src/routes/authz.ts +++ b/server/src/routes/authz.ts @@ -199,6 +199,7 @@ export function getActorInfo(req: Request): ( actorId: string; agentId: string | null; runId: string | null; + agentApiKeyId: string | null; actorSource: "agent_key" | "agent_jwt"; } | { @@ -207,6 +208,7 @@ export function getActorInfo(req: Request): ( sessionId: string | null; agentId: null; runId: string | null; + agentApiKeyId: null; actorSource: "local_implicit" | "session" | "board_key" | "cloud_tenant"; } ) { @@ -218,6 +220,7 @@ export function getActorInfo(req: Request): ( actorId: req.actor.agentId ?? "unknown-agent", agentId: req.actor.agentId ?? null, runId: req.actor.runId ?? null, + agentApiKeyId: req.actor.keyId ?? null, actorSource, }; } @@ -235,6 +238,7 @@ export function getActorInfo(req: Request): ( sessionId: req.actor.sessionId ?? null, agentId: null, runId: req.actor.runId ?? null, + agentApiKeyId: null, actorSource, }; } diff --git a/server/src/routes/cases.ts b/server/src/routes/cases.ts index 54c92f7e4e..55bdcba072 100644 --- a/server/src/routes/cases.ts +++ b/server/src/routes/cases.ts @@ -837,6 +837,7 @@ export function caseRoutes(db: Db, storage: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "case.document_annotation_thread_created", entityType: "case", entityId: caseRow.id, @@ -875,6 +876,7 @@ export function caseRoutes(db: Db, storage: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "case.document_annotation_comment_added", entityType: "case", entityId: caseRow.id, @@ -911,6 +913,7 @@ export function caseRoutes(db: Db, storage: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: thread.status === "resolved" ? "case.document_annotation_thread_resolved" : "case.document_annotation_thread_reopened", diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index 3402bf0cf8..688662c1fc 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -358,6 +358,7 @@ export function companyRoutes(db: Db, storage?: StorageService) { entityId: result.company.id, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.imported", details: { include: body.include ?? null, @@ -478,6 +479,7 @@ export function companyRoutes(db: Db, storage?: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.updated", entityType: "company", entityId: companyId, @@ -503,6 +505,7 @@ export function companyRoutes(db: Db, storage?: StorageService) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.branding_updated", entityType: "company", entityId: companyId, diff --git a/server/src/routes/company-skills.ts b/server/src/routes/company-skills.ts index 7094d3a9a8..bcc3151ffb 100644 --- a/server/src/routes/company-skills.ts +++ b/server/src/routes/company-skills.ts @@ -400,6 +400,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_input_created", entityType: "company_skill_test_input", entityId: result.id, @@ -429,6 +430,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_input_updated", entityType: "company_skill_test_input", entityId: result.id, @@ -455,6 +457,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_input_deleted", entityType: "company_skill_test_input", entityId: result.id, @@ -483,6 +486,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_template_created", entityType: "company_skill_test_run_template", entityId: result.id, @@ -511,6 +515,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_template_updated", entityType: "company_skill_test_run_template", entityId: result.id, @@ -536,6 +541,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_template_deleted", entityType: "company_skill_test_run_template", entityId: result.id, @@ -593,6 +599,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.created", entityType: "issue", entityId: created.id, @@ -630,6 +637,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_harness_issue_cleaned_up", entityType: "issue", entityId: issueId, @@ -643,9 +651,11 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_created", entityType: "company_skill_test_run", entityId: result.id, + issueId: result.issueId, details: { skillId, inputId: result.inputId, @@ -691,9 +701,11 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_cancelled", entityType: "company_skill_test_run", entityId: result.id, + issueId: result.issueId, details: { skillId, issueId: result.issueId }, }); res.json(result); @@ -727,9 +739,11 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_deleted", entityType: "company_skill_test_run", entityId: result.id, + issueId: result.issueId, details: { skillId, issueId: result.issueId }, }); res.json(result); @@ -750,6 +764,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_version_created", entityType: "company_skill_version", entityId: result.id, @@ -775,6 +790,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_starred", entityType: "company_skill", entityId: skillId, @@ -795,6 +811,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_unstarred", entityType: "company_skill", entityId: skillId, @@ -823,6 +840,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_forked", entityType: "company_skill", entityId: result.skill.id, @@ -859,6 +877,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_comment_created", entityType: "company_skill_comment", entityId: result.id, @@ -884,6 +903,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_comment_updated", entityType: "company_skill_comment", entityId: result.id, @@ -906,6 +926,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_comment_deleted", entityType: "company_skill_comment", entityId: result.id, @@ -956,6 +977,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_created", entityType: "company_skill", entityId: result.id, @@ -985,6 +1007,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_updated", entityType: "company_skill", entityId: result.id, @@ -1021,6 +1044,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_file_updated", entityType: "company_skill", entityId: skillId, @@ -1050,6 +1074,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_file_deleted", entityType: "company_skill", entityId: skillId, @@ -1080,6 +1105,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skills_imported", entityType: "company", entityId: companyId, @@ -1122,6 +1148,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: result.action === "created" ? "company.skill_catalog_installed" : "company.skill_catalog_updated", entityType: "company_skill", entityId: result.skill.id, @@ -1154,6 +1181,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skills_scanned", entityType: "company", entityId: companyId, @@ -1191,6 +1219,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_deleted", entityType: "company_skill", entityId: result.id, @@ -1222,6 +1251,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_audited", entityType: "company_skill", entityId: skillId, @@ -1259,6 +1289,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_update_installed", entityType: "company_skill", entityId: result.id, @@ -1299,6 +1330,7 @@ export function companySkillRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_reset", entityType: "company_skill", entityId: result.id, diff --git a/server/src/routes/execution-workspaces.ts b/server/src/routes/execution-workspaces.ts index 7d598f722a..a8a59130fb 100644 --- a/server/src/routes/execution-workspaces.ts +++ b/server/src/routes/execution-workspaces.ts @@ -458,6 +458,7 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: `execution_workspace.runtime_${action}`, entityType: "execution_workspace", entityId: existing.id, @@ -505,6 +506,7 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "execution_workspace.branch_reconciled", entityType: "execution_workspace", entityId: existing.id, @@ -735,6 +737,7 @@ export function executionWorkspaceRoutes(db: Db, opts: { pluginWorkerManager?: P actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "execution_workspace.updated", entityType: "execution_workspace", entityId: workspace.id, diff --git a/server/src/routes/file-resources.ts b/server/src/routes/file-resources.ts index 6ef567930f..ee80e755da 100644 --- a/server/src/routes/file-resources.ts +++ b/server/src/routes/file-resources.ts @@ -409,6 +409,7 @@ export function fileResourceRoutes(db: Db, opts: { entityId: req.params.issueId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, details: listActivityDetails({ outcome: result.state === "available" ? "success" : "unavailable", workspaceSelector: result.query.workspace, @@ -521,6 +522,7 @@ export function fileResourceRoutes(db: Db, opts: { entityId: req.params.issueId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, details: activityDetails({ outcome: "success", workspaceKind: result.workspaceKind, @@ -646,6 +648,7 @@ export function fileResourceRoutes(db: Db, opts: { entityId: req.params.issueId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, details: activityDetails({ outcome: "success", workspaceKind: result.resource.workspaceKind, @@ -695,6 +698,7 @@ export function fileResourceRoutes(db: Db, opts: { entityId: req.params.issueId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, details: activityDetails({ outcome: "success", workspaceKind: result.resource.workspaceKind, diff --git a/server/src/routes/folders.ts b/server/src/routes/folders.ts index ad4859c166..03194c4f25 100644 --- a/server/src/routes/folders.ts +++ b/server/src/routes/folders.ts @@ -40,6 +40,7 @@ export function folderRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "folder.created", entityType: "folder", entityId: created.id, @@ -65,6 +66,7 @@ export function folderRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "folder.personal_ensured", entityType: "folder", entityId: folder.id, @@ -90,6 +92,7 @@ export function folderRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "folder.updated", entityType: "folder", entityId: updated.id, @@ -109,6 +112,7 @@ export function folderRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "folder.item_moved", entityType: req.body.kind === "routine" ? "routine" : "company_skill", entityId: moved.itemId, @@ -133,6 +137,7 @@ export function folderRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "folder.moved", entityType: "folder", entityId: updated.id, @@ -157,6 +162,7 @@ export function folderRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "folder.deleted", entityType: "folder", entityId: deleted.id, diff --git a/server/src/routes/inbox-agent-policy.ts b/server/src/routes/inbox-agent-policy.ts index 1a18d8c0b9..de58cf75fb 100644 --- a/server/src/routes/inbox-agent-policy.ts +++ b/server/src/routes/inbox-agent-policy.ts @@ -51,6 +51,7 @@ export function inboxAgentPolicyRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "inbox.agent_policy_updated", entityType: "user_inbox_agent_policy", entityId: userId, diff --git a/server/src/routes/inbox-dismissals.ts b/server/src/routes/inbox-dismissals.ts index ceae79c19c..02cc8a4c40 100644 --- a/server/src/routes/inbox-dismissals.ts +++ b/server/src/routes/inbox-dismissals.ts @@ -78,6 +78,7 @@ export function inboxDismissalRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: dismissal.kind === "snooze" ? "inbox.snoozed" : "inbox.dismissed", entityType: "company", entityId: companyId, @@ -115,6 +116,7 @@ export function inboxDismissalRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "inbox.restored", entityType: "company", entityId: companyId, diff --git a/server/src/routes/instance-settings.ts b/server/src/routes/instance-settings.ts index 3e63de6af8..209cc477fb 100644 --- a/server/src/routes/instance-settings.ts +++ b/server/src/routes/instance-settings.ts @@ -57,6 +57,7 @@ export function instanceSettingsRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "instance.settings.updated", entityType: "instance_settings", entityId: updated.id, @@ -94,6 +95,7 @@ export function instanceSettingsRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "instance.settings.general_updated", entityType: "instance_settings", entityId: updated.id, @@ -132,6 +134,7 @@ export function instanceSettingsRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "instance.settings.experimental_updated", entityType: "instance_settings", entityId: updated.id, @@ -177,6 +180,7 @@ export function instanceSettingsRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "instance.settings.issue_graph_liveness_auto_recovery_run", entityType: "instance_settings", entityId: "default", diff --git a/server/src/routes/issue-tree-control.ts b/server/src/routes/issue-tree-control.ts index cb452c6390..ccd4bc61da 100644 --- a/server/src/routes/issue-tree-control.ts +++ b/server/src/routes/issue-tree-control.ts @@ -56,6 +56,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_control_previewed", entityType: "issue", entityId: root.id, @@ -92,6 +93,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_hold_created", entityType: "issue", entityId: root.id, @@ -117,6 +119,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_hold_run_interrupted", entityType: "heartbeat_run", entityId: heartbeatRunId, @@ -133,6 +136,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_hold_run_interrupt_failed", entityType: "heartbeat_run", entityId: heartbeatRunId, @@ -162,6 +166,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_hold_wakeup_deferred", entityType: "agent_wakeup_request", entityId: wakeup.id, @@ -183,6 +188,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_cancel_status_updated", entityType: "issue", entityId: root.id, @@ -224,6 +230,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_restore_status_updated", entityType: "issue", entityId: root.id, @@ -270,9 +277,11 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_restore_wakeup_requested", entityType: "heartbeat_run", entityId: wakeRun.id, + issueId: restoredIssue.id, details: { holdId: result.hold.id, rootIssueId: root.id, @@ -366,6 +375,7 @@ export function issueTreeControlRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.tree_hold_released", entityType: "issue", entityId: root.id, diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 64046210cb..932254e8cb 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -453,6 +453,7 @@ async function auditAgentIssueCreateAttributionSpoof(input: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: input.action === "rejected" ? "issue.attribution_spoof_rejected" : "issue.attribution_spoof_stripped", @@ -2865,9 +2866,11 @@ export function issueRoutes( actorId: input.actor.actorId, agentId: input.actor.agentId, runId: input.actor.runId, + agentApiKeyId: input.actor.agentApiKeyId, action: "heartbeat.cancelled", entityType: "heartbeat_run", entityId: cancelledRunId, + issueId: input.issue.id, details: { source: "issue_comment_scheduled_retry_superseded", issueId: input.issue.id, @@ -3244,6 +3247,7 @@ export function issueRoutes( actorId: input.actor.actorId, agentId: input.actor.agentId, runId: input.actor.runId, + agentApiKeyId: input.actor.agentApiKeyId, action: "issue.thread_interaction_expired", entityType: "issue", entityId: input.issue.id, @@ -3587,6 +3591,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.checkout_lock_adopted", entityType: "issue", entityId: issue.id, @@ -3808,6 +3813,7 @@ export function issueRoutes( actorId: input.actor.actorId, agentId: input.actor.agentId, runId: input.actor.runId, + agentApiKeyId: input.actor.agentApiKeyId, action: "issue.task_watchdog_followups_serialized", entityType: "issue", entityId: watchdogParent.id, @@ -5018,6 +5024,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "label.created", entityType: "label", entityId: label.id, @@ -5042,6 +5049,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "label.deleted", entityType: "label", entityId: removed.id, @@ -5443,6 +5451,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: created ? "issue.watchdog_created" : "issue.watchdog_updated", entityType: "issue", entityId: issue.id, @@ -5479,6 +5488,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.watchdog_removed", entityType: "issue", entityId: issue.id, @@ -5610,6 +5620,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.updated", entityType: "issue", entityId: result.issue.id, @@ -5631,6 +5642,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.recovery_action_resolved", entityType: "issue", entityId: result.issue.id, @@ -5754,6 +5766,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "external_object.refresh_requested", entityType: "issue", entityId: issue.id, @@ -5848,6 +5861,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_annotation_thread_created", entityType: "issue", entityId: issue.id, @@ -5926,6 +5940,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_annotation_comment_added", entityType: "issue", entityId: issue.id, @@ -5974,6 +5989,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: thread.status === "resolved" ? "issue.document_annotation_thread_resolved" : "issue.document_annotation_thread_reopened", @@ -6044,6 +6060,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: result.created ? "issue.document_created" : "issue.document_updated", entityType: "issue", entityId: issue.id, @@ -6069,6 +6086,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_annotation_remapped", entityType: "issue", entityId: issue.id, @@ -6145,6 +6163,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_locked", entityType: "issue", entityId: issue.id, @@ -6184,6 +6203,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_unlocked", entityType: "issue", entityId: issue.id, @@ -6256,6 +6276,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_restored", entityType: "issue", entityId: issue.id, @@ -6282,6 +6303,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_annotation_remapped", entityType: "issue", entityId: issue.id, @@ -6358,6 +6380,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.document_deleted", entityType: "issue", entityId: issue.id, @@ -6432,6 +6455,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.work_product_created", entityType: "issue", entityId: issue.id, @@ -6565,6 +6589,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.low_trust_output_promoted", entityType: "issue", entityId: issue.id, @@ -6629,6 +6654,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.work_product_updated", entityType: "issue", entityId: existing.issueId, @@ -6666,6 +6692,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.work_product_deleted", entityType: "issue", entityId: existing.issueId, @@ -6700,6 +6727,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.read_marked", entityType: "issue", entityId: issue.id, @@ -6728,6 +6756,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.read_unmarked", entityType: "issue", entityId: issue.id, @@ -6800,6 +6829,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.inbox_archived", entityType: "issue", entityId: issue.id, @@ -6826,6 +6856,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.inbox_unarchived", entityType: "issue", entityId: issue.id, @@ -6868,6 +6899,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.approval_linked", entityType: "issue", entityId: issue.id, @@ -6896,6 +6928,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.approval_unlinked", entityType: "issue", entityId: issue.id, @@ -7069,6 +7102,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.created", entityType: "issue", entityId: issue.id, @@ -7104,6 +7138,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.monitor_scheduled", entityType: "issue", entityId: issue.id, @@ -7127,6 +7162,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.watchdog_created", entityType: "issue", entityId: issue.id, @@ -7237,6 +7273,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.child_created", entityType: "issue", entityId: issue.id, @@ -7264,6 +7301,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.monitor_scheduled", entityType: "issue", entityId: issue.id, @@ -7288,6 +7326,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.watchdog_created", entityType: "issue", entityId: issue.id, @@ -7428,6 +7467,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.accepted_plan_decomposition_updated", entityType: "issue", entityId: sourceIssue.id, @@ -7456,6 +7496,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.child_created", entityType: "issue", entityId: issue.id, @@ -7483,6 +7524,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.monitor_scheduled", entityType: "issue", entityId: issue.id, @@ -7731,9 +7773,11 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "heartbeat.cancelled", entityType: "heartbeat_run", entityId: cancelled.id, + issueId: existing.id, details: { agentId: cancelled.agentId, source: "issue_comment_interrupt", @@ -7962,9 +8006,11 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "heartbeat.cancelled", entityType: "heartbeat_run", entityId: cancelled.id, + issueId: existing.id, details: { agentId: cancelled.agentId, source: "issue_status_cancelled", issueId: existing.id }, }); } @@ -7976,9 +8022,11 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "heartbeat.cancel_failed", entityType: "heartbeat_run", entityId: runToCancelForCancelledStatus.id, + issueId: existing.id, details: { source: "issue_status_cancelled", issueId: existing.id }, }); } @@ -8086,6 +8134,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.updated", entityType: "issue", entityId: issue.id, @@ -8129,6 +8178,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.successful_run_handoff_resolved", entityType: "issue", entityId: issue.id, @@ -8159,6 +8209,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.blockers_updated", entityType: "issue", entityId: issue.id, @@ -8187,6 +8238,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.reviewers_updated", entityType: "issue", entityId: issue.id, @@ -8207,6 +8259,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.approvers_updated", entityType: "issue", entityId: issue.id, @@ -8230,6 +8283,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.monitor_scheduled", entityType: "issue", entityId: issue.id, @@ -8252,6 +8306,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.monitor_cleared", entityType: "issue", entityId: issue.id, @@ -8298,9 +8353,11 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.skill_test_run_completed", entityType: "company_skill_test_run", entityId: completedRun.id, + issueId: issue.id, details: { issueId: issue.id, status: completedRun.status, @@ -8342,6 +8399,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.comment_added", entityType: "issue", entityId: issue.id, @@ -8689,6 +8747,7 @@ export function issueRoutes( actorId: "issue_update", agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.blockers_resolved_wake_emitted", entityType: "issue", entityId: dependentIssueId, @@ -8739,6 +8798,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.deleted", entityType: "issue", entityId: issue.id, @@ -8801,6 +8861,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.checked_out", entityType: "issue", entityId: issue.id, @@ -8856,6 +8917,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.released", entityType: "issue", entityId: released.id, @@ -8891,6 +8953,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.admin_force_release", entityType: "issue", entityId: result.issue.id, @@ -8988,6 +9051,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.thread_interaction_created", entityType: "issue", entityId: issue.id, @@ -9065,6 +9129,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: interaction.status === "expired" ? "issue.thread_interaction_expired" : "issue.thread_interaction_accepted", @@ -9092,6 +9157,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.updated", entityType: "issue", entityId: issue.id, @@ -9168,6 +9234,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: interaction.status === "expired" ? "issue.thread_interaction_expired" : "issue.thread_interaction_rejected", @@ -9221,6 +9288,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.thread_interaction_answered", entityType: "issue", entityId: issue.id, @@ -9275,6 +9343,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: interaction.status === "expired" ? "issue.thread_interaction_expired" : "issue.thread_interaction_item_verdicts_submitted", @@ -9336,6 +9405,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.thread_interaction_cancelled", entityType: "issue", entityId: issue.id, @@ -9426,6 +9496,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.comment_cancelled", entityType: "issue", entityId: issue.id, @@ -9495,6 +9566,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.comment_deleted", entityType: "issue", entityId: issue.id, @@ -9712,6 +9784,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.updated", entityType: "issue", entityId: currentIssue.id, @@ -9753,9 +9826,11 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "heartbeat.cancelled", entityType: "heartbeat_run", entityId: cancelled.id, + issueId: currentIssue.id, details: { agentId: cancelled.agentId, source: "issue_comment_interrupt", @@ -9873,6 +9948,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.updated", entityType: "issue", entityId: currentIssue.id, @@ -9924,6 +10000,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.comment_added", entityType: "issue", entityId: currentIssue.id, @@ -10190,6 +10267,7 @@ export function issueRoutes( actorId: "issue_comment", agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.blockers_resolved_wake_emitted", entityType: "issue", entityId: dependentIssueId, @@ -10238,6 +10316,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.feedback_vote_saved", entityType: "issue", entityId: issue.id, @@ -10258,6 +10337,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "company.feedback_data_sharing_updated", entityType: "company", entityId: issue.companyId, @@ -10279,6 +10359,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "instance.settings.general_updated", entityType: "instance_settings", entityId: settings.id, @@ -10394,6 +10475,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.attachment_added", entityType: "issue", entityId: issueId, @@ -10500,6 +10582,7 @@ export function issueRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "issue.attachment_removed", entityType: "issue", entityId: removed.issueId, diff --git a/server/src/routes/plugins.ts b/server/src/routes/plugins.ts index aa9c9882a5..566a200129 100644 --- a/server/src/routes/plugins.ts +++ b/server/src/routes/plugins.ts @@ -697,6 +697,7 @@ export function pluginRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action, entityType: "plugin", entityId, diff --git a/server/src/routes/resource-memberships.ts b/server/src/routes/resource-memberships.ts index 5a64d69e40..9212cdcf1f 100644 --- a/server/src/routes/resource-memberships.ts +++ b/server/src/routes/resource-memberships.ts @@ -34,6 +34,7 @@ async function logMembershipChange( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: `resource_membership.${input.changeKind}`, entityType: input.resourceType, entityId: input.resourceId, diff --git a/server/src/routes/routines.ts b/server/src/routes/routines.ts index 8a3082dead..a9f0e7eee6 100644 --- a/server/src/routes/routines.ts +++ b/server/src/routes/routines.ts @@ -68,6 +68,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.document_annotation_remapped", entityType: "routine", entityId: routineId, @@ -132,6 +133,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.revision_created", entityType: "routine", entityId: input.routineId, @@ -168,6 +170,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.created", entityType: "routine", entityId: created.id, @@ -259,6 +262,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.document_annotation_thread_created", entityType: "routine", entityId: routine.id, @@ -299,6 +303,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.document_annotation_comment_added", entityType: "routine", entityId: routine.id, @@ -337,6 +342,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: thread.status === "resolved" ? "routine.document_annotation_thread_resolved" : "routine.document_annotation_thread_reopened", @@ -392,6 +398,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.updated", entityType: "routine", entityId: routine.id, @@ -430,6 +437,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.revision_restored", entityType: "routine", entityId: routine.id, @@ -472,6 +480,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.trigger_created", entityType: "routine_trigger", entityId: created.trigger.id, @@ -512,6 +521,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.trigger_updated", entityType: "routine_trigger", entityId: trigger.id, @@ -553,6 +563,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.trigger_deleted", entityType: "routine_trigger", entityId: trigger.id, @@ -597,6 +608,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.trigger_secret_rotated", entityType: "routine_trigger", entityId: trigger.id, @@ -632,6 +644,7 @@ export function routineRoutes( actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "routine.run_triggered", entityType: "routine_run", entityId: run.id, diff --git a/server/src/routes/sidebar-preferences.ts b/server/src/routes/sidebar-preferences.ts index f7247c448a..40cffaa5db 100644 --- a/server/src/routes/sidebar-preferences.ts +++ b/server/src/routes/sidebar-preferences.ts @@ -55,6 +55,7 @@ export function sidebarPreferenceRoutes(db: Db) { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "sidebar_preferences.project_order_updated", entityType: "company", entityId: companyId, diff --git a/server/src/routes/smoke-lab.ts b/server/src/routes/smoke-lab.ts index 25f72ac8cf..04daa426c6 100644 --- a/server/src/routes/smoke-lab.ts +++ b/server/src/routes/smoke-lab.ts @@ -131,6 +131,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.services_started", entityType: "smoke_lab", entityId: companyId, @@ -151,6 +152,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.services_stopped", entityType: "smoke_lab", entityId: companyId, @@ -171,6 +173,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.fixtures_installed", entityType: "smoke_lab", entityId: companyId, @@ -204,6 +207,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.run_created", entityType: "smoke_run", entityId: run.id, @@ -231,6 +235,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.run_updated", entityType: "smoke_run", entityId: run.id, @@ -251,6 +256,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.step_recorded", entityType: "smoke_run_step", entityId: result.step.id, @@ -271,6 +277,7 @@ export function smokeLabRoutes(db: Db, options: { actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, + agentApiKeyId: actor.agentApiKeyId, action: "smoke_lab.reset", entityType: "smoke_lab", entityId: companyId, diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index 473a1d787b..fb27951a5c 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -1,7 +1,8 @@ import { randomUUID } from "node:crypto"; +import { and, eq } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import { activityLog } from "@paperclipai/db"; -import { PLUGIN_EVENT_TYPES, type PluginEventType } from "@paperclipai/shared"; +import { activityLog, agentApiKeys, companies, heartbeatRuns, issues } from "@paperclipai/db"; +import { isUuidLike, PLUGIN_EVENT_TYPES, type PluginEventType } from "@paperclipai/shared"; import type { PluginEvent } from "@paperclipai/plugin-sdk"; import { publishLiveEvent } from "./live-events.js"; import { redactCurrentUserValue } from "../log-redaction.js"; @@ -59,9 +60,70 @@ export interface LogActivityInput { entityId: string; agentId?: string | null; runId?: string | null; + agentApiKeyId?: string | null; + issueId?: string | null; details?: Record | null; } +function readNonEmptyString(value: unknown) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +export async function resolveResponsibleUserIdForActivity(db: Db, input: LogActivityInput) { + if (input.actorType === "user") return readNonEmptyString(input.actorId); + + const runId = readNonEmptyString(input.runId); + if (runId && isUuidLike(runId)) { + const run = await db + .select({ responsibleUserId: heartbeatRuns.responsibleUserId }) + .from(heartbeatRuns) + .where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, runId))) + .then((rows) => rows[0] ?? null); + const runResponsibleUserId = readNonEmptyString(run?.responsibleUserId); + if (runResponsibleUserId) return runResponsibleUserId; + } + + const issueIdCandidate = readNonEmptyString(input.issueId) + ?? (input.entityType === "issue" ? readNonEmptyString(input.entityId) : null); + const issueId = isUuidLike(issueIdCandidate) ? issueIdCandidate : null; + if (issueId) { + const issue = await db + .select({ + responsibleUserId: issues.responsibleUserId, + createdByUserId: issues.createdByUserId, + }) + .from(issues) + .where(and(eq(issues.companyId, input.companyId), eq(issues.id, issueId))) + .then((rows) => rows[0] ?? null); + const issueResponsibleUserId = readNonEmptyString(issue?.responsibleUserId) + ?? readNonEmptyString(issue?.createdByUserId); + if (issueResponsibleUserId) return issueResponsibleUserId; + } + + const agentApiKeyId = readNonEmptyString(input.agentApiKeyId); + const agentId = readNonEmptyString(input.agentId); + if (agentApiKeyId && isUuidLike(agentApiKeyId)) { + const apiKey = await db + .select({ responsibleUserId: agentApiKeys.responsibleUserId }) + .from(agentApiKeys) + .where(and( + eq(agentApiKeys.companyId, input.companyId), + eq(agentApiKeys.id, agentApiKeyId), + ...(agentId && isUuidLike(agentId) ? [eq(agentApiKeys.agentId, agentId)] : []), + )) + .then((rows) => rows[0] ?? null); + const apiKeyResponsibleUserId = readNonEmptyString(apiKey?.responsibleUserId); + if (apiKeyResponsibleUserId) return apiKeyResponsibleUserId; + } + + const company = await db + .select({ defaultResponsibleUserId: companies.defaultResponsibleUserId }) + .from(companies) + .where(eq(companies.id, input.companyId)) + .then((rows) => rows[0] ?? null); + return readNonEmptyString(company?.defaultResponsibleUserId); +} + export async function logActivity(db: Db, input: LogActivityInput) { const currentUserRedactionOptions = { enabled: (await instanceSettingsService(db).getGeneral()).censorUsernameInLogs, @@ -70,6 +132,7 @@ export async function logActivity(db: Db, input: LogActivityInput) { const redactedDetails = sanitizedDetails ? redactCurrentUserValue(sanitizedDetails, currentUserRedactionOptions) : null; + const responsibleUserId = await resolveResponsibleUserIdForActivity(db, input); await db.insert(activityLog).values({ companyId: input.companyId, actorType: input.actorType, @@ -79,6 +142,7 @@ export async function logActivity(db: Db, input: LogActivityInput) { entityId: input.entityId, agentId: input.agentId ?? null, runId: input.runId ?? null, + responsibleUserId, details: redactedDetails, }); @@ -93,6 +157,7 @@ export async function logActivity(db: Db, input: LogActivityInput) { entityId: input.entityId, agentId: input.agentId ?? null, runId: input.runId ?? null, + responsibleUserId, details: redactedDetails, }, }); @@ -112,6 +177,7 @@ export async function logActivity(db: Db, input: LogActivityInput) { ...redactedDetails, agentId: input.agentId ?? null, runId: input.runId ?? null, + responsibleUserId, }, }; publishPluginDomainEvent(event); diff --git a/server/src/services/environment-run-orchestrator.ts b/server/src/services/environment-run-orchestrator.ts index 1add1a2b24..e69d9ef669 100644 --- a/server/src/services/environment-run-orchestrator.ts +++ b/server/src/services/environment-run-orchestrator.ts @@ -291,6 +291,7 @@ export function environmentRunOrchestrator( action: "environment.lease_acquired", entityType: "environment_lease", entityId: leaseRecord.lease.id, + issueId: input.issueId, details: { environmentId: environment.id, driver: environment.driver, @@ -535,6 +536,7 @@ export function environmentRunOrchestrator( action: "environment.lease_released", entityType: "environment_lease", entityId: released.lease.id, + issueId: released.lease.issueId, details: { environmentId: released.lease.environmentId, driver: released.environment.driver, diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 49d99c598c..5cefb7a0dd 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -5740,6 +5740,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) action: "company.skill_test_run_completed", entityType: "company_skill_test_run", entityId: completedRun.id, + issueId: input.issueId, details: { issueId: input.issueId, status: completedRun.status, @@ -10673,6 +10674,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) action: "issue.tree_hold_run_interrupted", entityType: "heartbeat_run", entityId: run.id, + issueId: issueId, details: { issueId, holdId: activePauseHold.holdId, diff --git a/server/src/services/tool-gateway.ts b/server/src/services/tool-gateway.ts index a2c445a8d0..7ee58056d2 100644 --- a/server/src/services/tool-gateway.ts +++ b/server/src/services/tool-gateway.ts @@ -1181,6 +1181,7 @@ export function createToolGatewayService( entityId, agentId: input.agentId, runId: input.runId, + issueId: input.issueId, details: { gatewaySessionId: input.session?.id ?? null, gatewayId: input.session?.gatewayId ?? null,