feat: create credential providers before synthesizing a deploy - #2123
feat: create credential providers before synthesizing a deploy#2123notgitika wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
Nice PR. The design of running credential provisioning before cdk synth and threading the ARNs through deployed-state.json is well-motivated and the comments do a great job of capturing why. The seam boundary (IdentityProviderClient) is drawn at the SDK client rather than at fs/process boundaries, so the tests avoid excessive mocking while still exercising the real spec + .env.local parsing paths. Sharing credentialEnvVarName/CLIENT_SECRET_SUFFIX between add and deploy via envLocal.ts (with a re-export from shared.ts) removes a latent format-drift bug.
A couple of small things that aren't blockers but worth confirming intentional:
-
Stale credential entries when the spec goes to zero credentials. In
src/core/project/backends/cdk.ts(~L115)updateTargetStateis only called whenObject.keys(provisioned).length > 0. If a user deletes their last credential fromagentcore.jsonand re-deploys,provisionedis{}, the state write is skipped, and the previousresources.credentialsmap is left on disk. The doc-comment onupdateTargetStatepromises "A resource map provided in the patch replaces the previous map for that kind wholesale, so a credential dropped from the spec stops being advertised" — that guarantee is only actually delivered when at least one credential remains. Since the synthesized CDK app looks up credentials by name, this is likely inert in practice, but if you want the drop-to-zero case to behave the same as drop-one-of-many, you'd either always callupdateTargetState({ resources: { credentials: provisioned } })or explicitly write{}whendeclaredis non-empty on the spec side but you provisioned nothing. -
parseEnvcast inEnvLocalFile.read(src/core/project/envLocal.tsL90):parseEnv's declared return type isRecord<string, string | undefined>(last-write-wins across duplicate keys), but you cast toRecord<string, string>. All callers happen to useif (!value)so undefined is handled safely today; just be aware the type is a small lie and a future caller doingenv[k].trim()would compile but crash.
Neither of these needs to block the merge.
4b787a8 to
0db4266
Compare
The synthesized CDK app reads credential provider ARNs out of deployed-state.json and fails to synth a project that declares credentials until they exist. Provision them between the account preflight and the build, then record their ARNs via updateTargetState so the assembly is synthesized against a state file that already describes them. Providers are created when absent and reused when present, never updated, so a redeploy neither mints a new secret version nor overwrites one rotated outside the CLI. Payment credentials are rejected up front (agentcore.json can't express the vendor config they need). Secrets come from the same place 'project add credentials' writes them, so the env-var name is now derived from one function in envLocal.ts that both sides share.
0db4266 to
3c09c23
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #2123 +/- ##
==========================================
Coverage 97.37% 97.38%
==========================================
Files 455 456 +1
Lines 27741 27991 +250
==========================================
+ Hits 27014 27259 +245
- Misses 727 732 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Claude Security Review: no high-confidence findings. (run) |
…v type - Add SDK-mocked coverage for createIdentityProviderClient (the real Identity factory the provisioner tests bypass): ~55% -> ~95% on credentials.ts. - Always record the provisioned credential set, so removing the last credential from the spec clears the stale entry instead of leaving it advertised. - EnvLocalFile.read returns Record<string, string | undefined> (parseEnv's real type) rather than casting it away. - Tighten a few verbose comments.
|
Claude Security Review: no high-confidence findings. (run) |
|
Claude Security Review: no high-confidence findings. (run) |
|
Claude Security Review: no high-confidence findings. (run) |
aidandaly24
left a comment
There was a problem hiding this comment.
Mostly looks good to me just a few comments
| : [ | ||
| { | ||
| key: credentialEnvVarName(flags.name, "_CLIENT_SECRET"), | ||
| key: credentialEnvVarName(flags.name, CLIENT_SECRET_SUFFIX), |
There was a problem hiding this comment.
I think collision detection needs to compare the final per-type environment keys, including this suffix. I added an API key named foo_client_secret and OAuth credential named foo. Both additions succeeded, and provisioning sent the API-key value as both the API key and OAuth clientSecret because both resolve to AGENTCORE_CREDENTIAL_FOO_CLIENT_SECRET. Could the shared validation derive the actual secret key for each credential type and reject collisions before writing or touching AWS?
There was a problem hiding this comment.
good catch. I'll make the collision check derive each credential's actual secret key by type (api-key -> base, oauth -> base + _CLIENT_SECRET) and reject on the final key, before writing or touching AWS
|
|
||
| // Credential providers aren't stack resources; the synthesized app reads their | ||
| // ARNs from deployed-state.json, so they must exist and be recorded before synth. | ||
| const provisioned = yield* this.provisionCredentials(project, { |
There was a problem hiding this comment.
Could the local CDK prerequisite checks stay ahead of credential provisioning? I ran this with agentcore/cdk/node_modules missing: the provisioner ran and deployed-state.json was written, then deploy failed with the npm install guidance. Synthesis still has to happen after provisioning, but checking npm and dependencies separately first would avoid mutating AWS for a local setup error. Probably just good to have like for ux but not a blocker.
There was a problem hiding this comment.
Agreed, a local setup error shouldn't mutate AWS. I'll hoist the npm/node_modules prerequisite check ahead of credential provisioning so we fail fast before creating anything. Synth still runs after provisioning, but the local-only checks don't need to.
| // Provider names are account-global, so a name already taken by another | ||
| // project in this account is adopted rather than recreated. | ||
| yield { message: `Preparing credential provider '${credential.name}'` }; | ||
| provisioned[credential.name] = await provisionOne(client, credential, env, project.rootPath); |
There was a problem hiding this comment.
I think this can leave partial AWS state when provisioning fails midway.
For example:
- The first credential provider is created successfully.
- Creating the second provider fails.
provisionCredentials()throws, soupdateTargetState()is never reached.
The first provider now exists in AWS but is not recorded locally. Retrying with the same spec recovers it, but abandoning the deploy or removing that credential leaves the provider orphaned.
Could we validate all credential inputs before creating anything and roll back providers created by this invocation if a later creation fails?
There was a problem hiding this comment.
right, there's an orphan window. I'll validate all credential inputs up front (resolve every .env.local/secretRef before creating anything), which removes the most likely mid-loop failure before any AWS call. It's also self-healing on retry via reuse-by-name. I'm inclined to skip automatic rollback of already-created providers for now (extra delete calls + failure modes for an edge case retry already recovers), let me know if you'd rather I add it.
| * keeps provider configs secret-free, so the one vendor key it carries is the | ||
| * only place the secret can go. | ||
| */ | ||
| function vendorConfigWithSecret( |
There was a problem hiding this comment.
I really like the breakdown of functionality in this file.
| new CreateApiKeyCredentialProviderCommand({ | ||
| name, | ||
| ...(apiKey !== undefined && { apiKey }), | ||
| ...(secretRef && { apiKeySecretConfig: secretRef, apiKeySecretSource: "EXTERNAL" }), |
There was a problem hiding this comment.
When this points to an external secret, the generated Gateway role cannot actually read it. We save the returned ARN as clientSecretArn, but @aws/agentcore-cdk 0.1.0-alpha.45 only uses that ARN for OAuth targets. Its API-key path grants secretsmanager:GetSecretValue only for the managed bedrock-agentcore-identity!* secrets. That means a Gateway target using an API-key credential with secretRef can deploy successfully and then fail when it tries to retrieve the key. Could we grant the recorded external ARN for API-key targets as well, either here or by updating the pinned construct?
There was a problem hiding this comment.
I think the change is on the refactor branch tho right @notgitika ?
There was a problem hiding this comment.
I checked the l3 cdk constructs repo, it's not fixed on its refactor branch either. Gateway.ts there (same as main/alpha.45) grants the recorded external secret ARN for OAuth targets, but the API-key path grants GetSecretValue only on bedrock-agentcore-identity!* — so an API-key secretRef isn't covered.
That's a construct fix (broaden the api-key Gateway grant to include the recorded external ARN, like the OAuth path), not something the CLI can do, it already records the ARN.
Also worth noting the credential-provider CDK construct on that refactor branch is the "credentials via CDK" approach we're not going ahead with. I can file a follow-up on agentcore-l3-cdk-constructs and keep it out of scope for this PR
…ovisioning - Collision detection now compares each credential's actual per-type .env.local variable (API key uses the base name, OAuth appends _CLIENT_SECRET), so cross-type clashes like api-key 'foo_client_secret' vs oauth 'foo' are rejected before writing the spec. - Check local CDK prerequisites (npm + node_modules) before provisioning credentials, so a local setup error no longer mutates AWS. - Resolve every credential (look up existing, validate the secret) before creating any, so a missing secret fails before the first provider is created rather than leaving a half-provisioned state.
|
Claude Security Review: no high-confidence findings. (run) |
… lint - Keep the 'same environment variable' wording so the existing add-credentials collision test still asserts it, and add an integration test for the cross-type case (oauth 'foo' vs api-key 'foo_client_secret'). - Silence require-yield on a deploy-prereq spy generator that never runs.
|
Claude Security Review: no high-confidence findings. (run) |
| return { | ||
| customOauth2ProviderConfig: { | ||
| oauthDiscovery: { discoveryUrl: credential.discoveryUrl }, | ||
| ...(credential.clientId !== undefined && { clientId: credential.clientId }), |
There was a problem hiding this comment.
I think older OAuth projects need a compatibility fallback here. v0.28.0 stored the client ID only in AGENTCORE_CREDENTIAL_<NAME>_CLIENT_ID, not in agentcore.json. If the provider needs to be recreated in another target/account or after deletion, this sends the discovery URL and secret without the client ID. Would it make sense to prefer credential.clientId and fall back to the legacy environment variable, with an upgrade test?
Creates a project's credential providers before synthesis, so the synthesized CDK app can read their ARNs out of
deployed-state.jsonand wire them into the stack. Without this, deploying a project that declares any credential fails insidecdk synth.What it does
CdkBackend.deployprovisions each declared credential provider: reuse-if-present, create-if-absent, never update (so a redeploy neither mints a new secret version nor overwrites one rotated outside the CLI), then records the ARNs viaupdateTargetState.project add credentialswrites them —.env.local(AGENTCORE_CREDENTIAL_<NAME>) or a Secrets ManagersecretRef. The env-var name is now derived from one shared function inenvLocal.tssoaddanddeployagree.Note
Tested e2e
Ran the full flow against a real account (us-west-2):
project add credentials api-key→project deploy. Confirmed deploy creates the API-key credential provider imperatively before synth, writes it toagentcore/.cli/deployed-state.json, then deploys the stack and merges thestackArninto the same target entry (no clobbering). Resulting file held bothresources.credentials.e2ekeyandstackArn.