diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index 9cffa8f2a6..3728a77e7c 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -403,7 +403,16 @@ describe.sequential("agent skill routes", () => { opts?: { skipUserSecrets?: boolean }, ) => { expect(config).toBe(adapterConfig); - expect(context).toBeUndefined(); + // Audit-only actor context is threaded through for company `secret_ref` + // attribution; user secrets are still skipped (skipUserSecrets: true). + expect(context).toEqual({ + consumerType: "agent", + consumerId: "11111111-1111-4111-8111-111111111111", + actorType: "user", + actorId: "local-board", + actorSource: "local_implicit", + responsibleUserId: "local-board", + }); expect(opts).toEqual({ adapterType: "claude_local", skipUserSecrets: true }); return { config: { env: { HOME: "/home/agent" } } }; }, @@ -427,6 +436,51 @@ describe.sequential("agent skill routes", () => { ); }); + it("threads a non-undefined actor secret context into resolveAdapterConfigForRuntime on both skills routes (audit fidelity, skipUserSecrets preserved)", async () => { + const expectedContext = { + consumerType: "agent", + consumerId: "11111111-1111-4111-8111-111111111111", + actorType: "user", + actorId: "local-board", + actorSource: "local_implicit", + responsibleUserId: "local-board", + }; + + // GET /agents/:id/skills + mockAgentService.getById.mockResolvedValue(makeAgent("claude_local")); + const listRes = await requestApp( + await createApp(), + (baseUrl) => request(baseUrl) + .get("/api/agents/11111111-1111-4111-8111-111111111111/skills?companyId=company-1"), + ); + expect(listRes.status, JSON.stringify(listRes.body)).toBe(200); + const listCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1); + expect(listCall?.[2]).toBeDefined(); + expect(listCall?.[2]).toEqual(expectedContext); + expect(listCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true }); + + // POST /agents/:id/skills/sync + mockAdapter.syncSkills.mockResolvedValue({ + adapterType: "claude_local", + supported: true, + mode: "ephemeral", + desiredSkills: ["paperclipai/paperclip/paperclip"], + entries: [], + warnings: [], + }); + const syncRes = await requestApp( + await createApp(), + (baseUrl) => request(baseUrl) + .post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1") + .send({ desiredSkills: ["paperclip"] }), + ); + expect(syncRes.status, JSON.stringify(syncRes.body)).toBe(200); + const syncCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1); + expect(syncCall?.[2]).toBeDefined(); + expect(syncCall?.[2]).toEqual(expectedContext); + expect(syncCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true }); + }); + it("skips runtime materialization when listing Codex skills", async () => { mockAgentService.getById.mockResolvedValue(makeAgent("codex_local")); mockAdapter.listSkills.mockResolvedValue({ @@ -662,7 +716,16 @@ describe.sequential("agent skill routes", () => { type: "user_secret_ref", key: "github_pat_read_only", }); - expect(context).toBeUndefined(); + // Audit-only actor context is threaded through for company `secret_ref` + // attribution; user secrets are still skipped (skipUserSecrets: true). + expect(context).toEqual({ + consumerType: "agent", + consumerId: "11111111-1111-4111-8111-111111111111", + actorType: "user", + actorId: "local-board", + actorSource: "local_implicit", + responsibleUserId: "local-board", + }); expect(opts).toEqual({ adapterType: "claude_local", skipUserSecrets: true }); return { config: { diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index f034ce5324..32fbf97476 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -1860,7 +1860,7 @@ export function agentRoutes( const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime( agent.companyId, agent.adapterConfig, - undefined, + buildActorSecretContext(req, { consumerType: "agent", consumerId: agent.id }), { adapterType: agent.adapterType, skipUserSecrets: true }, ); const runtimeSkillConfig = await buildRuntimeSkillConfig( @@ -1925,7 +1925,7 @@ export function agentRoutes( const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime( updated.companyId, updated.adapterConfig, - undefined, + buildActorSecretContext(req, { consumerType: "agent", consumerId: updated.id }), { adapterType: updated.adapterType, skipUserSecrets: true }, ); const runtimeSkillConfig = {