From ddd3b09bd1e6f2185a79604616cad310cba11bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 23 May 2026 22:38:31 -0400 Subject: [PATCH] Redact employee references during GDPR delete --- .../src/main/services/compliance-service.ts | 44 ++++++++++++++++++- .../tests/unit/compliance-service.test.ts | 32 ++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/AgentCompany-New/src/main/services/compliance-service.ts b/AgentCompany-New/src/main/services/compliance-service.ts index b857bbf6..337b2bf1 100644 --- a/AgentCompany-New/src/main/services/compliance-service.ts +++ b/AgentCompany-New/src/main/services/compliance-service.ts @@ -26,6 +26,7 @@ export class ComplianceService { if (input.exportType === COMPLIANCE_EXPORT_TYPE.GDPR_DELETE) this.assertDeleteAllowed(input); const payload = this.subjectPayload(input.orgId, input.subjectType, input.subjectId); if (input.exportType === COMPLIANCE_EXPORT_TYPE.GDPR_DELETE) this.applyGdprDelete(input.orgId, input.subjectType, input.subjectId); + const requestedByEmployeeId = deletedEmployeeRequestor(input) ? null : input.requestedByEmployeeId; const id = createId("cmp"); const now = new Date().toISOString(); this.db.prepare(` @@ -41,7 +42,7 @@ export class ComplianceService { input.exportType, COMPLIANCE_EXPORT_STATE.COMPLETED, JSON.stringify(payload), - input.requestedByEmployeeId, + requestedByEmployeeId, now, now, ); @@ -152,6 +153,12 @@ export class ComplianceService { chatMessages: this.db.prepare("select * from chat_messages where org_id = ? and sender_type = 'employee' and sender_id = ? order by created_at").all(orgId, subjectId), approvalsRequested: this.db.prepare("select * from approvals where org_id = ? and requested_by_type = 'employee' and requested_by_id = ? order by created_at").all(orgId, subjectId), approvalsDecided: this.db.prepare("select * from approvals where org_id = ? and decided_by_employee_id = ? order by created_at").all(orgId, subjectId), + employeeIdentities: this.db.prepare("select * from employee_identities where org_id = ? and employee_id = ? order by created_at").all(orgId, subjectId), + roleGrants: this.db.prepare("select * from role_grants where org_id = ? and principal_type = 'employee' and principal_id = ? order by created_at").all(orgId, subjectId), + socialAccountsAuthorized: this.db.prepare("select * from social_accounts where org_id = ? and authorized_employee_id = ? order by created_at").all(orgId, subjectId), + socialAccountGrants: this.db.prepare("select * from social_account_grants where org_id = ? and employee_id = ? order by created_at").all(orgId, subjectId), + scheduledPostsCreated: this.db.prepare("select * from scheduled_posts where org_id = ? and created_by_employee_id = ? order by created_at").all(orgId, subjectId), + complianceExportsRequested: this.db.prepare("select * from compliance_exports where org_id = ? and requested_by_employee_id = ? order by created_at").all(orgId, subjectId), auditEventsAsActor: this.db.prepare("select * from audit_events where org_id = ? and actor_type = 'employee' and actor_id = ? order by created_at").all(orgId, subjectId), }; } @@ -172,6 +179,35 @@ export class ComplianceService { .run(orgId, subjectId); this.db.prepare("update approvals set requested_by_id = null where org_id = ? and requested_by_type = 'employee' and requested_by_id = ?") .run(orgId, subjectId); + this.db.prepare("update approvals set decided_by_employee_id = null where org_id = ? and decided_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("delete from employee_identities where org_id = ? and employee_id = ?").run(orgId, subjectId); + this.db.prepare("delete from chat_read_state where org_id = ? and employee_id = ?").run(orgId, subjectId); + this.db.prepare("delete from social_account_grants where org_id = ? and employee_id = ?").run(orgId, subjectId); + this.db.prepare("update social_accounts set authorized_employee_id = null where org_id = ? and authorized_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update social_approval_routes set route_to_id = null where org_id = ? and route_to_type = 'authorized_employee' and route_to_id = ?") + .run(orgId, subjectId); + this.db.prepare("update scheduled_posts set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update compliance_exports set requested_by_employee_id = null where org_id = ? and requested_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update legal_holds set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update retention_freezes set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update audit_replay_runs set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update rollback_snapshots set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("update migration_reconciliation_items set resolved_by_employee_id = null where org_id = ? and resolved_by_employee_id = ?") + .run(orgId, subjectId); + this.db.prepare("delete from approval_assignments where org_id = ? and assignee_type = 'employee' and assignee_id = ?") + .run(orgId, subjectId); + this.db.prepare("delete from role_grants where org_id = ? and principal_type = 'employee' and principal_id = ?") + .run(orgId, subjectId); + this.db.prepare("update role_grants set granted_by_employee_id = null where org_id = ? and granted_by_employee_id = ?") + .run(orgId, subjectId); return; } this.db.prepare("update agents set name = ?, role = ?, lifecycle = ?, model_provider_id = null, model_id = null where id = ? and org_id = ?") @@ -309,3 +345,9 @@ export class ComplianceService { if (!row) throw new Error(message); } } + +function deletedEmployeeRequestor(input: CreateComplianceExportInput): boolean { + return input.exportType === COMPLIANCE_EXPORT_TYPE.GDPR_DELETE + && input.subjectType === "employee" + && input.subjectId === input.requestedByEmployeeId; +} diff --git a/AgentCompany-New/tests/unit/compliance-service.test.ts b/AgentCompany-New/tests/unit/compliance-service.test.ts index 6fd871ef..ba18f653 100644 --- a/AgentCompany-New/tests/unit/compliance-service.test.ts +++ b/AgentCompany-New/tests/unit/compliance-service.test.ts @@ -3,10 +3,13 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { DatabaseConnection } from "../../src/main/database/connection"; +import { ApprovalService } from "../../src/main/services/approval-service"; import { ChatService } from "../../src/main/services/chat-service"; import { ComplianceService } from "../../src/main/services/compliance-service"; import { OrganizationService } from "../../src/main/services/organization-service"; import { SnapshotService } from "../../src/main/services/snapshot-service"; +import { SocialService } from "../../src/main/services/social-service"; +import { APPROVAL_STATE, SOCIAL_PLATFORM } from "../../src/shared/domain"; function createConnection(): DatabaseConnection { return new DatabaseConnection(join(tmpdir(), `agentcompany-new-compliance-${randomUUID()}.db`)); @@ -64,13 +67,17 @@ describe("compliance service", () => { test("blocks GDPR delete under holds and redacts after release", () => { const connection = createConnection(); const organization = new OrganizationService(connection.db); + const approvals = new ApprovalService(connection.db); const chat = new ChatService(connection.db); const compliance = new ComplianceService(connection.db); + const social = new SocialService(connection.db); const snapshot = new SnapshotService(connection.db); let orgId = ""; let employeeId = ""; let spaceId = ""; let holdId = ""; + let approvalId = ""; + let socialAccountId = ""; connection.transaction(() => { orgId = organization.createOrganization({ @@ -82,6 +89,24 @@ describe("compliance service", () => { employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; spaceId = chat.createSpace({ orgId, spaceType: "project", name: "release", topic: "Release coordination", visibility: "workspace" }); chat.sendMessage({ orgId, spaceId, threadId: null, senderType: "employee", senderId: employeeId, bodyMd: "Delete me", attachmentFileIds: [] }); + approvalId = approvals.create({ + orgId, + kind: "security_review", + title: "Security review", + details: "Employee requested and decided.", + risk: "low", + requestedByType: "employee", + requestedById: employeeId, + }); + approvals.decide({ orgId, approvalId, state: APPROVAL_STATE.APPROVED, decidedByEmployeeId: employeeId }); + socialAccountId = social.createAccount({ + orgId, + platform: SOCIAL_PLATFORM.X, + handle: "@acme", + displayName: "Acme", + createdByEmployeeId: employeeId, + authorizedEmployeeId: employeeId, + }); holdId = compliance.createLegalHold({ orgId, entityType: "employee", @@ -111,9 +136,16 @@ describe("compliance service", () => { }); const state = snapshot.load(); + const redactedApproval = state.approvals.find((approval) => approval.id === approvalId); + const redactedSocialAccount = state.socialAccounts.find((account) => account.id === socialAccountId); expect(state.employees[0]?.displayName).toBe("Deleted employee"); expect(state.chatMessages[0]?.senderId).toBeNull(); + expect(redactedApproval?.requestedById).toBeNull(); + expect(redactedApproval?.decidedByEmployeeId).toBeNull(); + expect(redactedSocialAccount?.authorizedEmployeeId).toBeNull(); + expect(state.socialAccountGrants).toHaveLength(0); expect(state.complianceExports[0]?.exportType).toBe("gdpr_delete"); + expect(state.complianceExports[0]?.requestedByEmployeeId).toBeNull(); expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "gdpr_deleted")).toBe(true); connection.close(); });