mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
[codex] Preserve AWS secret provider error classification (#10946)
## Thinking Path > - Paperclip is the open source control plane operators use to manage AI-agent companies. > - Operators can store managed secret values in AWS Secrets Manager provider vaults. > - The AWS JSON gateway already classifies safe provider failures such as access denial, expired credentials, throttling, and invalid requests. > - The outer provider wrapper was re-normalizing those typed failures, which collapsed an AWS access denial into a generic provider error before the service could produce actionable guidance. > - Missing runtime credentials also need to be recognized as an access problem so operators receive the correct recovery path. > - This pull request preserves typed provider errors, expands credential-related classification, and clarifies the safe credential/IAM remediation text. > - The benefit is that failed AWS secret creation tells operators whether to restore runtime credentials or grant the required capability without exposing AWS account or principal details. ## Linked Issues or Issue Description No public issue was found for this exact remaining server-side classification gap. Bug report: - What happened: an AWS Secrets Manager access-denied response could be classified correctly by the JSON gateway and then collapsed to `provider_error` by the outer provider wrapper; missing runtime credentials were also reported generically. - Expected behavior: safe typed AWS failures should retain their classification, and credential resolution failures should surface as access-denied guidance. - Steps to reproduce: configure an AWS provider vault with invalid or missing runtime credentials, or without `secretsmanager:CreateSecret`, then create a Paperclip-managed secret. - Paperclip version/commit: current `master` before this PR. - Deployment mode: Paperclip server with an AWS Secrets Manager provider vault. Related prior work: - Refs #9161 - Refs #9645 ## What Changed - Preserve `SecretProviderClientError` instances returned by the AWS JSON gateway instead of wrapping them as generic provider failures. - Classify missing, expired, invalid, or incorrectly signed AWS runtime credentials as access-denied failures. - Clarify sanitized remediation guidance to cover both credential resolution and `secretsmanager:CreateSecret` authorization. - Add regression coverage for gateway access-denied responses, missing runtime credentials, and the service-level actionable message. ## Verification - `pnpm exec vitest run server/src/__tests__/aws-secrets-manager-provider.test.ts server/src/__tests__/secrets-service.test.ts` — changed provider tests passed; the service file was skipped in the first no-DB focused run. - `pnpm --filter @paperclipai/server typecheck` — passed. - `pnpm -r typecheck` — passed after a frozen-lockfile dependency refresh. - `pnpm build` — passed. - `pnpm test:run` — 314 files / 3,443 tests passed; one unrelated existing test failed in `plugin-orchestration-apis.test.ts` because it observed `heartbeat.scheduling_suppressed` instead of `issue_commented`. The same failure reproduced in isolation and none of this PR's four changed files participate in that test. - `git diff --check public-gh/master...HEAD` — passed. ## Risks Low risk. The change affects only error classification and sanitized failure guidance for AWS Secrets Manager operations. Successful secret writes, schemas, migrations, API shapes, and stored secret material are unchanged. The regression tests assert safe classifications while keeping raw AWS account and principal details out of service responses. > 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 family (exact runtime build and context-window size were not exposed), with reasoning, repository editing, shell execution, Git, GitHub, and test execution enabled. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes (not applicable: no documented contract changed) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: commitperclip[bot] <284812584+commitperclip[bot]@users.noreply.github.com>
This commit is contained in:
4 files changed
+108
-2
No files matched your search
@@ -231,6 +231,102 @@ describe("awsSecretsManagerProvider", () => {
|
||||
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
|
||||
it("preserves classified AWS create errors returned by the JSON gateway", async () => {
|
||||
delete process.env.AWS_PROFILE;
|
||||
delete process.env.AWS_DEFAULT_PROFILE;
|
||||
delete process.env.AWS_CONFIG_FILE;
|
||||
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
||||
delete process.env.AWS_SDK_LOAD_CONFIG;
|
||||
process.env.AWS_ACCESS_KEY_ID = "AKIA_CREATE_ERROR_TEST";
|
||||
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
__type: "AccessDeniedException",
|
||||
message:
|
||||
"User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:CreateSecret",
|
||||
}),
|
||||
{ status: 400 },
|
||||
),
|
||||
);
|
||||
const provider = createAwsSecretsManagerProvider({
|
||||
config: {
|
||||
region: "us-west-1",
|
||||
endpoint: "https://secretsmanager.us-west-1.amazonaws.com",
|
||||
deploymentId: "prod",
|
||||
prefix: "paperclip",
|
||||
kmsKeyId: null,
|
||||
environmentTag: "production",
|
||||
providerOwnerTag: "paperclip",
|
||||
deleteRecoveryWindowDays: 30,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.createSecret({
|
||||
value: "super-secret-value",
|
||||
context: {
|
||||
companyId: "company-1",
|
||||
secretKey: "openai-api-key",
|
||||
secretName: "OpenAI API Key",
|
||||
version: 1,
|
||||
},
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "access_denied",
|
||||
status: 403,
|
||||
operation: "CreateSecret",
|
||||
message: "AWS Secrets Manager denied the request. Check IAM permissions for this provider vault.",
|
||||
rawMessage: expect.stringContaining("AccessDeniedException"),
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies missing runtime AWS credentials as access denied", async () => {
|
||||
const provider = createAwsSecretsManagerProvider({
|
||||
config: {
|
||||
region: "us-east-1",
|
||||
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
||||
deploymentId: "prod-use1",
|
||||
prefix: "paperclip",
|
||||
kmsKeyId: null,
|
||||
environmentTag: "production",
|
||||
providerOwnerTag: "paperclip",
|
||||
deleteRecoveryWindowDays: 30,
|
||||
},
|
||||
gateway: {
|
||||
async createSecret() {
|
||||
throw new Error("CredentialsProviderError: Could not load credentials from any providers");
|
||||
},
|
||||
async putSecretValue() {
|
||||
throw new Error("not used");
|
||||
},
|
||||
async getSecretValue() {
|
||||
throw new Error("not used");
|
||||
},
|
||||
async deleteSecret() {
|
||||
throw new Error("not used");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.createSecret({
|
||||
value: "super-secret-value",
|
||||
context: {
|
||||
companyId: "company-1",
|
||||
secretKey: "openai-api-key",
|
||||
secretName: "OpenAI API Key",
|
||||
version: 1,
|
||||
},
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "access_denied",
|
||||
status: 403,
|
||||
rawMessage: "CredentialsProviderError: Could not load credentials from any providers",
|
||||
});
|
||||
});
|
||||
|
||||
it("creates new AWS secret versions against a namespace-valid existing secret reference", async () => {
|
||||
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
||||
const provider = createAwsSecretsManagerProvider({
|
||||
|
||||
@@ -3409,6 +3409,9 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
requiredCapability: "secretsmanager:CreateSecret",
|
||||
},
|
||||
});
|
||||
expect((thrown as HttpError).details?.actionableMessage).toContain(
|
||||
"Ensure the Paperclip server runtime can resolve valid AWS credentials",
|
||||
);
|
||||
expect(JSON.stringify(thrown)).not.toContain("arn:aws");
|
||||
expect(JSON.stringify(thrown)).not.toContain("123456789012");
|
||||
expect(thrown instanceof Error ? thrown.message : String(thrown)).not.toContain("arn:aws");
|
||||
|
||||
@@ -840,7 +840,11 @@ function asAwsSecretsManagerMaterial(value: StoredSecretVersionMaterial): AwsSec
|
||||
function classifyAwsProviderError(message: string): SecretProviderClientErrorCode {
|
||||
if (/ResourceExistsException|AlreadyExists/i.test(message)) return "conflict";
|
||||
if (/ResourceNotFoundException|NotFound/i.test(message)) return "not_found";
|
||||
if (/AccessDeniedException|AccessDenied|UnrecognizedClientException|InvalidClientTokenId|not authorized/i.test(message)) {
|
||||
if (
|
||||
/AccessDeniedException|AccessDenied|CredentialsProviderError|Could not load credentials|credential provider chain|UnrecognizedClientException|InvalidClientTokenId|ExpiredTokenException|InvalidSignatureException|SignatureDoesNotMatch|not authorized/i.test(
|
||||
message,
|
||||
)
|
||||
) {
|
||||
return "access_denied";
|
||||
}
|
||||
if (/Throttl|TooManyRequests|RequestLimitExceeded|Rate exceeded/i.test(message)) return "throttled";
|
||||
@@ -870,6 +874,9 @@ function awsProviderSafeMessage(code: SecretProviderClientErrorCode): string {
|
||||
}
|
||||
|
||||
function normalizeAwsError(operation: string, error: unknown): never {
|
||||
if (error instanceof SecretProviderClientError) {
|
||||
throw error;
|
||||
}
|
||||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||||
const code = classifyAwsProviderError(rawMessage);
|
||||
throw new SecretProviderClientError({
|
||||
|
||||
@@ -505,7 +505,7 @@ function safeRemoteProviderErrorDetails(
|
||||
if (context.operation === "secret.create") {
|
||||
details.requiredCapability = "secretsmanager:CreateSecret";
|
||||
details.actionableMessage =
|
||||
"AWS managed secret creation needs secretsmanager:CreateSecret in the selected region for this provider vault. If the vault config uses a KMS key, the runtime credentials also need KMS write permissions for that key.";
|
||||
"Ensure the Paperclip server runtime can resolve valid AWS credentials, then grant them secretsmanager:CreateSecret in the selected region for this provider vault. If the vault config uses a KMS key, those credentials also need KMS write permissions for that key.";
|
||||
details.safeAlternative =
|
||||
"If the secret already exists in AWS, link it as an external reference instead of creating a Paperclip-managed value.";
|
||||
} else if (context.operation === "secret.rotate") {
|
||||
|
||||
Reference in new issue
Block a user