diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index e9c2cc9f..37ac15ee 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -121,7 +121,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { }); ipcMain.handle(IPC_CHANNELS.launchProviderSignIn, (_event, input) => { try { - providers.launchSignInWindow(input.orgId, input.providerId); + providers.launchSignInWindow(input); return ok(null); } catch (error) { return fail("provider_sign_in_failed", error instanceof Error ? error.message : String(error)); diff --git a/AgentCompany-New/src/main/services/provider-service.ts b/AgentCompany-New/src/main/services/provider-service.ts index 4861d986..e784738c 100644 --- a/AgentCompany-New/src/main/services/provider-service.ts +++ b/AgentCompany-New/src/main/services/provider-service.ts @@ -1,5 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; -import type { CancelProviderResponseInput, CreateProviderResponseInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, StreamProviderResponseInput, UpdateProviderPolicyInput } from "../../shared/commands"; +import type { CancelProviderResponseInput, CreateProviderResponseInput, LaunchProviderSignInInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, StreamProviderResponseInput, UpdateProviderPolicyInput } from "../../shared/commands"; import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, type ProviderAuthType, type ProviderExecutionMode } from "../../shared/domain"; import type { ProviderStreamRendererEvent } from "../../shared/provider-request-domain"; import { PROVIDER_CATALOG } from "../../shared/provider-catalog"; @@ -14,7 +14,7 @@ import { ProviderRateLimitRegistry } from "./provider-rate-limit-registry"; import { ProviderResponseObserver } from "./provider-response-observer"; import { hashPrompt, readProviderResponseId, requireStreamText } from "./provider-response-utils"; import { ProviderRequestStore } from "./provider-request-store"; -import { launchProviderSignInWindow } from "./provider-sign-in-window"; +import { launchProviderSignInWindow, type LaunchProviderSignInWindowInput } from "./provider-sign-in-window"; import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain"; const AUTH_STATE_CONFIGURED = "configured"; @@ -41,6 +41,7 @@ export class ProviderService { private readonly db: DatabaseSync, private readonly adapters: ProviderAdapterRegistry = providerAdapters, vault: ProviderSecretVault = new SecretVault(), + private readonly launchSignIn: (input: LaunchProviderSignInWindowInput) => void = launchProviderSignInWindow, ) { this.audit = new AuditService(db); this.authBroker = new AuthBrokerService(db, vault); @@ -141,23 +142,24 @@ export class ProviderService { } } - launchSignInWindow(orgId: string, providerId: string): void { + launchSignInWindow(input: LaunchProviderSignInInput): void { + this.assertEmployee(input.orgId, input.employeeId); const provider = this.db.prepare(` select id, provider_key, label, auth_type from providers where id = ? and org_id = ? - `).get(providerId, orgId) as { id: string; provider_key: string; label: string; auth_type: string } | undefined; + `).get(input.providerId, input.orgId) as { id: string; provider_key: string; label: string; auth_type: string } | undefined; if (!provider) throw new Error("Provider was not found."); const catalogEntry = this.requireCatalogEntry(provider.provider_key); if (!catalogEntry.signInUrl) throw new Error(`${provider.label} does not support browser sign-in.`); - launchProviderSignInWindow({ providerId: provider.id, label: provider.label, signInUrl: catalogEntry.signInUrl }); + this.launchSignIn({ providerId: provider.id, label: provider.label, signInUrl: catalogEntry.signInUrl }); this.audit.record({ - orgId, + orgId: input.orgId, entityType: "provider", entityId: provider.id, actorType: "employee", - actorId: null, + actorId: input.employeeId, action: "sign_in_window_opened", payload: { providerKey: provider.provider_key, authType: provider.auth_type }, }); @@ -465,6 +467,12 @@ export class ProviderService { return entry; } + private assertEmployee(orgId: string, employeeId: string): void { + const employee = this.db.prepare("select id from employees where id = ? and org_id = ?") + .get(employeeId, orgId) as { id: string } | undefined; + if (!employee) throw new Error("Provider sign-in employee was not found."); + } + private recordProviderHealth(providerId: string, orgId: string, healthState: string, error: string | null): void { this.db.prepare(` update providers diff --git a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx index 44581c1b..4b67f6b4 100644 --- a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx @@ -131,6 +131,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) { {tab === "providers" && ( (null); const [signInLaunched, setSignInLaunched] = useState(false); const connected = providers.filter((provider) => provider.enabled); @@ -60,7 +61,7 @@ export function ProvidersPanel({ orgId, providers, catalog, agents, providerRate const launchSignIn = () => { if (!active) return; setSignInLaunched(true); - void actions.run(() => window.agentCompany.launchProviderSignIn({ orgId, providerId: active.id })); + void actions.run(() => window.agentCompany.launchProviderSignIn({ orgId, providerId: active.id, employeeId })); }; return ( diff --git a/AgentCompany-New/src/shared/commands.ts b/AgentCompany-New/src/shared/commands.ts index 336f36e3..c6d7ba8a 100644 --- a/AgentCompany-New/src/shared/commands.ts +++ b/AgentCompany-New/src/shared/commands.ts @@ -170,6 +170,7 @@ export interface RevokeProviderCredentialInput { export interface LaunchProviderSignInInput { orgId: string; providerId: string; + employeeId: string; } export interface CheckProviderHealthInput { diff --git a/AgentCompany-New/tests/unit/provider-service.test.ts b/AgentCompany-New/tests/unit/provider-service.test.ts index a49274eb..4ed6efb5 100644 --- a/AgentCompany-New/tests/unit/provider-service.test.ts +++ b/AgentCompany-New/tests/unit/provider-service.test.ts @@ -169,6 +169,46 @@ describe("provider service", () => { connection.close(); }); + test("audits provider sign-in launches with the employee actor", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const snapshot = new SnapshotService(connection.db); + let launched: { providerId: string; label: string; signInUrl: string } | null = null; + const providers = new ProviderService(connection.db, providerAdapters, testVault, (input) => { launched = input; }); + let orgId = ""; + let employeeId = ""; + let copilotProviderId = ""; + + connection.transaction(() => { + orgId = createOrganization(organization); + employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; + copilotProviderId = providerId(connection, orgId, "github-copilot"); + }); + + expect(() => providers.launchSignInWindow({ + orgId, + providerId: copilotProviderId, + employeeId: "emp_missing", + })).toThrow("Provider sign-in employee was not found."); + + connection.transaction(() => providers.launchSignInWindow({ orgId, providerId: copilotProviderId, employeeId })); + + const state = snapshot.load(); + expect(launched).toMatchObject({ + providerId: copilotProviderId, + label: "GitHub Copilot", + signInUrl: "https://github.com/login", + }); + expect(state.auditEvents).toContainEqual(expect.objectContaining({ + entityType: "provider", + entityId: copilotProviderId, + actorType: "employee", + actorId: employeeId, + action: "sign_in_window_opened", + })); + connection.close(); + }); + test("records provider request results through direct execution adapters", async () => { const connection = createConnection(); const organization = new OrganizationService(connection.db);