forked from farhoodlabs/paperclip
[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:  AWS discovery candidate flow:  Provider vault removal confirmation:  ## 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:
@@ -12,6 +12,7 @@ export const API = {
|
||||
approvals: `${API_PREFIX}/approvals`,
|
||||
secrets: `${API_PREFIX}/secrets`,
|
||||
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
|
||||
secretProviderConfigDiscoveryPreview: `${API_PREFIX}/companies/:companyId/secret-provider-configs/discovery/preview`,
|
||||
costs: `${API_PREFIX}/costs`,
|
||||
activity: `${API_PREFIX}/activity`,
|
||||
dashboard: `${API_PREFIX}/dashboard`,
|
||||
|
||||
@@ -558,6 +558,10 @@ export type {
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
SecretProviderConfigHealthResponse,
|
||||
SecretProviderConfigDiscoveryCandidate,
|
||||
SecretProviderConfigDiscoveryPreviewResult,
|
||||
SecretProviderConfigDiscoverySample,
|
||||
SecretProviderConfigDiscoverySignal,
|
||||
CompanySecretBinding,
|
||||
CompanySecretBindingTarget,
|
||||
CompanySecretUsageBinding,
|
||||
@@ -885,6 +889,7 @@ export {
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
secretProviderConfigDiscoveryPreviewSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
remoteSecretImportSelectionSchema,
|
||||
@@ -911,6 +916,7 @@ export {
|
||||
type CreateSecret,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type SecretProviderConfigDiscoveryPreview,
|
||||
type RemoteSecretImportPreview,
|
||||
type RemoteSecretImport,
|
||||
type RemoteSecretImportSelection,
|
||||
|
||||
@@ -258,6 +258,10 @@ export type {
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
SecretProviderConfigHealthResponse,
|
||||
SecretProviderConfigDiscoveryCandidate,
|
||||
SecretProviderConfigDiscoveryPreviewResult,
|
||||
SecretProviderConfigDiscoverySample,
|
||||
SecretProviderConfigDiscoverySignal,
|
||||
CompanySecretBinding,
|
||||
CompanySecretBindingTarget,
|
||||
CompanySecretUsageBinding,
|
||||
|
||||
@@ -138,6 +138,43 @@ export interface SecretProviderConfigHealthResponse {
|
||||
checkedAt: Date;
|
||||
}
|
||||
|
||||
export interface SecretProviderConfigDiscoverySignal {
|
||||
namespace: string | null;
|
||||
secretNamePrefix: string | null;
|
||||
environmentTag: string | null;
|
||||
ownerTag: string | null;
|
||||
kmsKeyId: string | null;
|
||||
hasKmsKey: boolean;
|
||||
sampleCount: number;
|
||||
paperclipManagedSampleCount: number;
|
||||
skippedForeignPaperclipSampleCount: number;
|
||||
}
|
||||
|
||||
export interface SecretProviderConfigDiscoverySample {
|
||||
name: string;
|
||||
hasKmsKey: boolean;
|
||||
tagKeys: string[];
|
||||
}
|
||||
|
||||
export interface SecretProviderConfigDiscoveryCandidate {
|
||||
provider: SecretProvider;
|
||||
displayName: string;
|
||||
config: SecretProviderConfigPayload;
|
||||
sampleCount: number;
|
||||
samples: SecretProviderConfigDiscoverySample[];
|
||||
signals: SecretProviderConfigDiscoverySignal;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface SecretProviderConfigDiscoveryPreviewResult {
|
||||
provider: SecretProvider;
|
||||
nextToken: string | null;
|
||||
sampledSecretCount: number;
|
||||
skippedForeignPaperclipSampleCount: number;
|
||||
candidates: SecretProviderConfigDiscoveryCandidate[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface CompanySecretVersion {
|
||||
id: string;
|
||||
secretId: string;
|
||||
|
||||
@@ -293,6 +293,7 @@ export {
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
secretProviderConfigDiscoveryPreviewSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
remoteSecretImportSelectionSchema,
|
||||
@@ -309,6 +310,7 @@ export {
|
||||
type CreateSecret,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type SecretProviderConfigDiscoveryPreview,
|
||||
type RemoteSecretImportPreview,
|
||||
type RemoteSecretImport,
|
||||
type RemoteSecretImportSelection,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createSecretSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
secretProviderConfigDiscoveryPreviewSchema,
|
||||
secretProviderConfigPayloadSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
} from "./secret.js";
|
||||
@@ -140,6 +141,40 @@ describe("secret validators", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("validates AWS provider vault discovery draft config without allowing sensitive keys", () => {
|
||||
expect(
|
||||
secretProviderConfigDiscoveryPreviewSchema.parse({
|
||||
provider: "aws_secrets_manager",
|
||||
config: {
|
||||
region: "us-east-1",
|
||||
namespace: "production",
|
||||
secretNamePrefix: "paperclip",
|
||||
},
|
||||
query: "paperclip",
|
||||
pageSize: 50,
|
||||
}),
|
||||
).toEqual({
|
||||
provider: "aws_secrets_manager",
|
||||
config: {
|
||||
region: "us-east-1",
|
||||
namespace: "production",
|
||||
secretNamePrefix: "paperclip",
|
||||
},
|
||||
query: "paperclip",
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
secretProviderConfigDiscoveryPreviewSchema.parse({
|
||||
provider: "aws_secrets_manager",
|
||||
config: {
|
||||
region: "us-east-1",
|
||||
accessKeyId: "AKIA...",
|
||||
},
|
||||
}),
|
||||
).toThrow(/sensitive field/i);
|
||||
});
|
||||
|
||||
it("caps AWS remote import paging and row counts", () => {
|
||||
expect(() =>
|
||||
remoteSecretImportPreviewSchema.parse({
|
||||
|
||||
@@ -262,6 +262,30 @@ export const remoteSecretImportPreviewSchema = z.object({
|
||||
|
||||
export type RemoteSecretImportPreview = z.infer<typeof remoteSecretImportPreviewSchema>;
|
||||
|
||||
export const secretProviderConfigDiscoveryPreviewSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS),
|
||||
config: z.record(z.unknown()).default({}),
|
||||
query: z.string().trim().max(200).optional().nullable(),
|
||||
nextToken: z.string().trim().min(1).max(4096).optional().nullable(),
|
||||
pageSize: z.number().int().min(1).max(100).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
rejectSensitiveProviderConfigKeys(value.config, ctx);
|
||||
const parsed = secretProviderConfigPayloadSchema.safeParse({
|
||||
provider: value.provider,
|
||||
config: value.config,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.error.issues) {
|
||||
ctx.addIssue({
|
||||
...issue,
|
||||
path: issue.path[0] === "config" ? issue.path : ["config", ...issue.path],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type SecretProviderConfigDiscoveryPreview = z.infer<typeof secretProviderConfigDiscoveryPreviewSchema>;
|
||||
|
||||
export const remoteSecretImportSelectionSchema = z.object({
|
||||
externalRef: z.string().trim().min(1).max(2048),
|
||||
name: z.string().trim().min(1).max(160).optional().nullable(),
|
||||
|
||||
Reference in New Issue
Block a user