diff --git a/cli/src/__tests__/teams.test.ts b/cli/src/__tests__/teams.test.ts index defbeb48425..379f8d9bfdc 100644 --- a/cli/src/__tests__/teams.test.ts +++ b/cli/src/__tests__/teams.test.ts @@ -386,6 +386,8 @@ describe("teams CLI commands", () => { "manager-1", "--collision-strategy", "rename", + "--secret-value", + "agent:cto:OPENAI_API_KEY=sk-live-secret", "--company-id", "company-1", "--request-approval-on-forbidden", @@ -406,6 +408,7 @@ describe("teams CLI commands", () => { body: JSON.stringify({ targetManagerAgentId: "manager-1", collisionStrategy: "rename", + secretValues: { "agent:cto:OPENAI_API_KEY": "sk-live-secret" }, }), }), ); @@ -443,8 +446,15 @@ describe("teams CLI commands", () => { companyId: "company-1", catalogRef: "product-engineering", deniedReason: "Missing permission: can create agents", + options: { + targetManagerAgentId: "manager-1", + collisionStrategy: "rename", + secretValues: { "agent:cto:OPENAI_API_KEY": "[redacted]" }, + }, }, }); + expect(JSON.stringify(approvalPayload)).not.toContain("sk-live-secret"); + expect(String(logSpy.mock.calls[0]?.[0])).not.toContain("sk-live-secret"); expect(errorSpy).not.toHaveBeenCalled(); }); diff --git a/cli/src/commands/client/teams.ts b/cli/src/commands/client/teams.ts index e5acdde6ed4..3a49cf83346 100644 --- a/cli/src/commands/client/teams.ts +++ b/cli/src/commands/client/teams.ts @@ -373,6 +373,7 @@ const INSTALL_APPROVAL_FALLBACK_MESSAGES = [ "missing permission: agents:create", "missing permission: can create agents", ]; +const SECRET_VALUE_REDACTION = "[redacted]"; function shouldRequestInstallApproval(error: unknown, opts: TeamInstallOptions): error is ApiRequestError { if (!(opts.requestApprovalOnForbidden || isPaperclipTaskRun())) return false; @@ -395,6 +396,8 @@ async function requestInstallApproval( if (!ctx.companyId) throw new Error("Company ID is required."); const trimmedRef = catalogRef.trim(); const issueIds = resolveApprovalIssueIds(opts); + const approvalInstallOptions = omitInstallSecretValues(installOptions); + const returnedInstallOptions = redactInstallSecretValues(installOptions); const payload = { type: "request_board_approval", issueIds, @@ -412,7 +415,7 @@ async function requestInstallApproval( installAttempt: { companyId: ctx.companyId, catalogRef: trimmedRef, - options: installOptions, + options: approvalInstallOptions, deniedReason: error.message, }, }, @@ -427,12 +430,28 @@ async function requestInstallApproval( installAttempt: { companyId: ctx.companyId, catalogRef: trimmedRef, - options: installOptions, + options: returnedInstallOptions, deniedReason: error.message, }, }; } +function omitInstallSecretValues(options: CatalogTeamInstallOptions): CatalogTeamInstallOptions { + if (!options.secretValues) return options; + const { secretValues: _secretValues, ...safeOptions } = options; + return safeOptions; +} + +function redactInstallSecretValues(options: CatalogTeamInstallOptions): CatalogTeamInstallOptions { + if (!options.secretValues) return options; + return { + ...options, + secretValues: Object.fromEntries( + Object.keys(options.secretValues).map((key) => [key, SECRET_VALUE_REDACTION]), + ), + }; +} + function resolveApprovalIssueIds(opts: TeamInstallOptions): string[] | undefined { const issueId = opts.approvalIssueId?.trim() || process.env.PAPERCLIP_TASK_ID?.trim(); if (!issueId) return undefined;