[codex] Provider vault secrets UX (#6381)

## Thinking Path

> - Paperclip orchestrates AI agents that need scoped, auditable access
to secrets
> - Hosted and external deployments need provider vault configuration
without exposing secret values in Paperclip metadata
> - AWS Secrets Manager vault setup previously required too much manual
operator knowledge
> - Provider vault discovery and removal belong together as an
independent secrets-management improvement
> - This pull request adds AWS provider vault discovery/prefill plus
vault removal flows
> - The benefit is a safer operator path for configuring external secret
storage before higher-level cloud workflows depend on it

## What Changed

- Added shared validators/types for AWS provider vault discovery
payloads and safe provider metadata.
- Implemented AWS provider vault discovery preview on the server.
- Added provider vault removal service/route behavior.
- Added Secrets page UI for discovery prefill, removal messaging, and
related rendering coverage.
- Added Storybook provider-vault fixtures and captured screenshots for
the new UX states.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm exec vitest run packages/shared/src/validators/secret.test.ts
server/src/__tests__/aws-secrets-manager-provider.test.ts
server/src/__tests__/secrets-routes.test.ts
server/src/__tests__/secrets-service.test.ts
ui/src/pages/Secrets.render.test.tsx`
- Result: 4 files passed, 1 embedded Postgres-backed file skipped on
this host because local Postgres init was unavailable.
- `pnpm --filter @paperclipai/ui exec vitest run
src/pages/Secrets.render.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- Storybook screenshot capture against `Product/Secrets` on
`http://127.0.0.1:60381/iframe.html?id=product-secrets--secrets-inventory&viewMode=story&globals=theme:dark`

## Screenshots

Provider vaults tab after this change:

![Provider vaults
tab](https://raw.githubusercontent.com/paperclipai/paperclip/pap-9861-provider-vault-secrets/doc/screenshots/pr-6381/provider-vaults-tab.png)

AWS discovery candidate flow:

![AWS discovery candidate
flow](https://raw.githubusercontent.com/paperclipai/paperclip/pap-9861-provider-vault-secrets/doc/screenshots/pr-6381/aws-discovery-candidates.png)

Provider vault removal confirmation:

![Provider vault removal
confirmation](https://raw.githubusercontent.com/paperclipai/paperclip/pap-9861-provider-vault-secrets/doc/screenshots/pr-6381/remove-provider-vault-confirmation.png)

## Risks

- Secret provider metadata handling must remain non-sensitive;
validators reject credential-bearing Vault URLs and sensitive AWS
discovery keys.
- AWS discovery depends on deployment credentials being configured
correctly outside Paperclip-managed company secrets.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent with local shell/git/tool use.
Exact hosted model ID and context-window size are not exposed by the
local Paperclip adapter runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-05-19 15:50:23 -05:00 committed by GitHub
parent 9c29394f4d
commit d67347be77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1602 additions and 13 deletions

View file

@ -22,6 +22,8 @@ import {
storybookProjects,
storybookSecretAccessEvents,
storybookSecretBindings,
storybookSecretProviderConfigs,
storybookSecretProviderDiscoveryPreview,
storybookSecretProviderHealth,
storybookSecretProviders,
storybookSecrets,
@ -187,6 +189,20 @@ function installStorybookApiFixtures() {
return Response.json(storybookSecretProviderHealth);
}
const secretProviderConfigsMatch = url.pathname.match(
/^\/api\/companies\/([^/]+)\/secret-provider-configs$/,
);
if (secretProviderConfigsMatch) {
return Response.json(storybookSecretProviderConfigs);
}
const secretProviderConfigDiscoveryPreviewMatch = url.pathname.match(
/^\/api\/companies\/([^/]+)\/secret-provider-configs\/discovery\/preview$/,
);
if (secretProviderConfigDiscoveryPreviewMatch && init?.method?.toUpperCase() === "POST") {
return Response.json(storybookSecretProviderDiscoveryPreview);
}
const secretUsageMatch = url.pathname.match(/^\/api\/secrets\/([^/]+)\/usage$/);
if (secretUsageMatch) {
const [, secretId] = secretUsageMatch;

View file

@ -7,6 +7,7 @@ import type {
Company,
CompanySecret,
CompanySecretBinding,
CompanySecretProviderConfig,
DashboardSummary,
ExecutionWorkspace,
Goal,
@ -15,6 +16,7 @@ import type {
IssueLabel,
Project,
SecretAccessEvent,
SecretProviderConfigDiscoveryPreviewResult,
SecretProviderDescriptor,
SidebarBadges,
WorkspaceRuntimeService,
@ -1324,6 +1326,98 @@ export const storybookSecretProviders: SecretProviderDescriptor[] = [
{ id: "vault", label: "HashiCorp Vault", requiresExternalRef: false },
];
export const storybookSecretProviderConfigs: CompanySecretProviderConfig[] = [
{
id: "provider-config-local",
companyId: "company-storybook",
provider: "local_encrypted",
displayName: "Local encrypted default",
status: "ready",
isDefault: true,
config: { backupReminderAcknowledged: true },
healthStatus: "ready",
healthCheckedAt: recent(45),
healthMessage: "Local encrypted provider is healthy.",
healthDetails: null,
disabledAt: null,
createdByAgentId: null,
createdByUserId: "user-board",
createdAt: recent(2_400),
updatedAt: recent(45),
},
{
id: "provider-config-aws-prod",
companyId: "company-storybook",
provider: "aws_secrets_manager",
displayName: "AWS production",
status: "warning",
isDefault: false,
config: {
region: "us-east-1",
namespace: "prod-use1",
secretNamePrefix: "paperclip",
kmsKeyId: "alias/paperclip-secrets",
ownerTag: "platform",
environmentTag: "production",
},
healthStatus: "warning",
healthCheckedAt: recent(18),
healthMessage: "Connected; KMS key rotation policy not yet enforced.",
healthDetails: {
code: "kms_rotation_policy",
message: "Connected; KMS key rotation policy not yet enforced.",
guidance: ["Enable automatic key rotation before using this vault for production agents."],
},
disabledAt: null,
createdByAgentId: null,
createdByUserId: "user-board",
createdAt: recent(1_800),
updatedAt: recent(18),
},
];
export const storybookSecretProviderDiscoveryPreview: SecretProviderConfigDiscoveryPreviewResult = {
provider: "aws_secrets_manager",
nextToken: null,
sampledSecretCount: 6,
skippedForeignPaperclipSampleCount: 1,
warnings: ["Skipped 1 Paperclip-managed AWS secret from a different deployment namespace."],
candidates: [
{
provider: "aws_secrets_manager",
displayName: "AWS production",
config: {
region: "us-east-1",
namespace: "prod-use1",
secretNamePrefix: "paperclip",
kmsKeyId: "alias/paperclip-secrets",
ownerTag: "platform",
environmentTag: "production",
},
sampleCount: 5,
samples: [
{
name: "paperclip/prod-use1/company-storybook/openai_api_key",
hasKmsKey: true,
tagKeys: ["paperclip:managed-by", "paperclip:environment", "paperclip:provider-owner"],
},
],
signals: {
namespace: "prod-use1",
secretNamePrefix: "paperclip",
environmentTag: "production",
ownerTag: "platform",
kmsKeyId: "alias/paperclip-secrets",
hasKmsKey: true,
sampleCount: 5,
paperclipManagedSampleCount: 5,
skippedForeignPaperclipSampleCount: 1,
},
warnings: [],
},
],
};
export const storybookSecrets: CompanySecret[] = [
{
id: "secret-openai",

View file

@ -22,7 +22,6 @@ if (typeof window !== "undefined") {
function StorybookSecretsFixtures({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
// Seed query caches synchronously so children hydrate from cache on first render.
queryClient.setQueryData(queryKeys.companies.all, storybookCompanies);
queryClient.setQueryData(queryKeys.secrets.list(COMPANY_ID), storybookSecrets);
const { selectedCompanyId, setSelectedCompanyId } = useCompany();