[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

@ -9,10 +9,12 @@ const mockSecretService = vi.hoisted(() => ({
listProviders: vi.fn(),
checkProviders: vi.fn(),
listProviderConfigs: vi.fn(),
previewProviderConfigDiscovery: vi.fn(),
getProviderConfigById: vi.fn(),
createProviderConfig: vi.fn(),
updateProviderConfig: vi.fn(),
disableProviderConfig: vi.fn(),
removeProviderConfig: vi.fn(),
setDefaultProviderConfig: vi.fn(),
checkProviderConfigHealth: vi.fn(),
getById: vi.fn(),
@ -117,6 +119,22 @@ describe("secret routes", () => {
expect(mockSecretService.listProviderConfigs).not.toHaveBeenCalled();
});
it("rejects provider vault discovery preview for non-board actors", async () => {
const res = await request(createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
}))
.post("/api/companies/company-1/secret-provider-configs/discovery/preview")
.send({
provider: "aws_secrets_manager",
config: { region: "us-east-1" },
});
expect(res.status).toBe(403);
expect(mockSecretService.previewProviderConfigDiscovery).not.toHaveBeenCalled();
});
it("rejects sensitive provider vault config fields", async () => {
const res = await request(createApp()).post("/api/companies/company-1/secret-provider-configs").send({
provider: "aws_secrets_manager",
@ -132,6 +150,92 @@ describe("secret routes", () => {
expect(mockSecretService.createProviderConfig).not.toHaveBeenCalled();
});
it("rejects sensitive provider vault discovery draft config fields", async () => {
const res = await request(createApp())
.post("/api/companies/company-1/secret-provider-configs/discovery/preview")
.send({
provider: "aws_secrets_manager",
config: {
region: "us-east-1",
secretAccessKey: "secret",
},
});
expect(res.status).toBe(400);
expect(JSON.stringify(res.body)).toMatch(/sensitive field/i);
expect(mockSecretService.previewProviderConfigDiscovery).not.toHaveBeenCalled();
});
it("previews provider vault discovery and logs only aggregate metadata", async () => {
mockSecretService.previewProviderConfigDiscovery.mockResolvedValue({
provider: "aws_secrets_manager",
nextToken: null,
sampledSecretCount: 2,
skippedForeignPaperclipSampleCount: 0,
candidates: [
{
provider: "aws_secrets_manager",
displayName: "AWS production",
config: {
region: "us-east-1",
namespace: "prod-use1",
secretNamePrefix: "paperclip",
environmentTag: "production",
ownerTag: "platform",
kmsKeyId: null,
},
sampleCount: 2,
samples: [
{ name: "paperclip/prod-use1/company-1/openai", hasKmsKey: false, tagKeys: ["environment"] },
],
signals: {
namespace: "prod-use1",
secretNamePrefix: "paperclip",
environmentTag: "production",
ownerTag: "platform",
kmsKeyId: null,
hasKmsKey: false,
sampleCount: 2,
paperclipManagedSampleCount: 0,
skippedForeignPaperclipSampleCount: 0,
},
warnings: [],
},
],
warnings: [],
});
const res = await request(createApp())
.post("/api/companies/company-1/secret-provider-configs/discovery/preview")
.send({
provider: "aws_secrets_manager",
config: { region: "us-east-1" },
query: "paperclip",
pageSize: 25,
});
expect(res.status).toBe(200);
expect(mockSecretService.previewProviderConfigDiscovery).toHaveBeenCalledWith("company-1", {
provider: "aws_secrets_manager",
config: { region: "us-east-1" },
query: "paperclip",
nextToken: undefined,
pageSize: 25,
});
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
action: "secret_provider_config.discovery_previewed",
entityType: "secret_provider_config_discovery",
entityId: "company-1",
details: {
provider: "aws_secrets_manager",
candidateCount: 1,
sampledSecretCount: 2,
warningCount: 0,
},
}));
expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("paperclip/prod-use1/company-1/openai");
});
it("rejects ready status for coming-soon provider vaults", async () => {
const res = await request(createApp()).post("/api/companies/company-1/secret-provider-configs").send({
provider: "vault",
@ -241,6 +345,48 @@ describe("secret routes", () => {
expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("accessKey");
});
it("removes provider vault config locally without deleting remote provider data", async () => {
const createdAt = new Date("2026-05-06T00:00:00.000Z");
const providerConfig = {
id: "11111111-1111-4111-8111-111111111111",
companyId: "company-1",
provider: "aws_secrets_manager",
displayName: "AWS prod",
status: "ready",
isDefault: false,
config: { region: "us-east-1" },
healthStatus: null,
healthCheckedAt: null,
healthMessage: null,
healthDetails: null,
disabledAt: null,
createdByAgentId: null,
createdByUserId: "user-1",
createdAt,
updatedAt: createdAt,
};
mockSecretService.getProviderConfigById.mockResolvedValue(providerConfig);
mockSecretService.removeProviderConfig.mockResolvedValue(providerConfig);
const res = await request(createApp()).delete(
"/api/secret-provider-configs/11111111-1111-4111-8111-111111111111",
);
expect(res.status).toBe(200);
expect(mockSecretService.removeProviderConfig).toHaveBeenCalledWith(
"11111111-1111-4111-8111-111111111111",
);
expect(mockSecretService.disableProviderConfig).not.toHaveBeenCalled();
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
action: "secret_provider_config.removed",
details: {
provider: "aws_secrets_manager",
displayName: "AWS prod",
remoteDeleted: false,
},
}));
});
it("rejects remote import preview for non-board actors", async () => {
const res = await request(createApp({
type: "agent",