feat: persist minimal deploy state to deployed-state.json - #2105
Conversation
Move the deploy-state file from agentcore/.cli/deployed-state.json to a committed agentcore/deployed-state.json, and stop storing a full snapshot of every resource. State is now keyed per target and holds only the deployed CloudFormation stack ARN (captured from the toolkit deploy result) plus the imperatively-created credential ARNs the synth step needs. Everything else is read live from CloudFormation, so the file never goes stale. Adds a DeployedState schema with readDeployedState/updateTargetState (merge-not-clobber, preserving sibling targets and unowned keys), surfaces stackArn from the CDK toolkit runner, and points the vended CDK app at the new path.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## refactor #2105 +/- ##
=========================================
Coverage 97.36% 97.37%
=========================================
Files 424 425 +1
Lines 25571 25641 +70
=========================================
+ Hits 24897 24967 +70
Misses 674 674 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Changes requested
The CDK app asset now reads agentcore/deployed-state.json, but the currently active deploy pipeline still writes credential ARNs to the old agentcore/.cli/deployed-state.json. The new CdkBackend/updateTargetState isn't wired into any production code path (no new CdkBackend( outside tests), so nothing writes to the new location during a real deploy. That breaks any project using OAuth or payment credentials.
Serious issue: split-brain read/write paths on the live deploy path
src/assets/cdk/bin/cdk.ts:131now readspath.join(configRoot, 'deployed-state.json').- Pre-synth identity setup in
src/cli/commands/deploy/actions.ts:213-223(and the mirror insrc/cli/tui/screens/deploy/useDeployFlow.ts:295-309) still writes viaconfigIO.writeDeployedState(...), which resolves to<baseDir>/.cli/deployed-state.jsonviaPathResolver.getStatePath()(src/lib/schemas/io/path-resolver.ts:195). - Post-deploy state persistence in
src/cli/commands/deploy/actions.ts:417-432writes to the same.cli/path.
Concretely, in the current production flow (deploy actions.ts → synthesizeCdk → bin/cdk.ts):
- Pre-deploy writes credential ARNs to
agentcore/.cli/deployed-state.json. - CDK synth reads
agentcore/deployed-state.json— which is either absent or stale (falls into thecatchand leavesdeployedStateundefined). credentialsresolves toundefined. For a project withpayments, this throws with a misleading “Runagentcore deployso the credential provider is created first” error atsrc/assets/cdk/bin/cdk.ts:180-186on the first (and every) deploy. For OAuth/API-key harness bindings, the ARN is silently dropped from the synthesized stack.
Fix options (choose one; each needs to land in the same PR as the asset change to avoid a regression):
- Point
PathResolver.getStatePath()at the new top-level path (src/lib/schemas/io/path-resolver.ts:195) so every existingconfigIO.readDeployedState/writeDeployedStatecaller (deploy actions, TUI deploy flow, teardown, status, invoke, session, imports, dev flows, evals, fetch-access, etc.) automatically usesagentcore/deployed-state.json. Also update the.gitignoretemplate atsrc/cli/operations/init/files.ts:13-19— the.cli/*+!.cli/deployed-state.jsonexception no longer matches the new path, and existing projects on disk still have.cli/deployed-state.jsonand will need either a migration or a compatibility read. - Revert the read-path change in
src/assets/cdk/bin/cdk.tsand defer the top-level move until the surrounding CLI is migrated in the same or a preceding change.
Either way, please add coverage that exercises the real read/write pairing (a bin/cdk.ts-level test, or an integration test that runs pre-deploy identity setup and then re-reads via the same path the asset uses). The current unit tests all wire updateTargetState to itself, so the mismatch with the live writers isn't caught.
Minor notes (not blockers)
- The comment at
src/assets/cdk/bin/cdk.ts:127-128says the file is “committed, not the gitignored.cli/dir,” but the old file was already committed via the!.cli/deployed-state.jsonexception in the init.gitignore. Consider rewording to reflect the real motivation (top-level visibility / minimal-schema restart). - Existing projects on disk will have stale
agentcore/.cli/deployed-state.jsonafter upgrading. Worth deciding whether to auto-migrate on next command, warn, or just document.
|
Claude Security Review: no high-confidence findings. (run) |
|
just ignore the harness reviewer :/ |
| return JSON.parse(await Bun.file(statePath(root)).text()); | ||
| } | ||
|
|
||
| describe("readDeployedState", () => { |
There was a problem hiding this comment.
is there a way to test this behavior through the handlers? The advantage being that implementation details here could change, but we still observe the same behavior e2e.
If that isn't really possible, than maybe one level higher in cdk?
| * 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. | ||
| */ | ||
| export async function updateTargetState( |
There was a problem hiding this comment.
It's a bit hard to see if we need these utilities without seeing where they're used. It seems like the real purpose of this PR is to add the needed DeployedState types. Is that right?
aidandaly24
left a comment
There was a problem hiding this comment.
I found several deployed-state integrity cases that I think need to be addressed before this merges.
| let deployedState: Record<string, unknown> | undefined; | ||
| try { | ||
| deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); | ||
| deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, 'deployed-state.json'), 'utf8')); |
There was a problem hiding this comment.
I think malformed deployed state can leave us in a bad partial state here. This catches the parse error and synthesis continues, so AWS deployment can complete before updateTargetState() rereads the same invalid file and throws. The command then reports failure after mutating AWS without recording the new stack ARN. We should only treat a missing file as absent and validate existing state before deployment.
| patch: Partial<TargetState>, | ||
| ): Promise<DeployedState> { | ||
| const statePath = statePathFor(projectRoot); | ||
| const state = await readDeployedState(json, projectRoot); |
There was a problem hiding this comment.
The “never drops another target” guarantee only holds for sequential updates. I reproduced two concurrent updates both reading empty state and the final file containing only prod. If concurrent deployments are not supported, I think we should remove or qualify this guarantee rather than imply the merge is generally safe.
There was a problem hiding this comment.
i qualified the comment
| * Result of a CDK operation. `stackArn` is the ARN of the deployed stack (only | ||
| * a deploy produces one); bootstrap leaves it undefined. | ||
| */ | ||
| export type CdkRunResult = { outputs: CdkOutputs; stackArn?: string }; |
There was a problem hiding this comment.
stackArn should probably be required for deploy results. CDK’s DeployedStack defines it as required, but CdkRunResult makes it optional and the backend silently skips persistence when it is absent. That leaves a successful deployment that later commands cannot resolve. I think we should fail instead of accepting a deploy result without the ARN.
| */ | ||
| export const DEPLOYED_STATE_RELATIVE_PATH = join("agentcore", "deployed-state.json"); | ||
|
|
||
| const CredentialStateSchema = z.object({ |
There was a problem hiding this comment.
One edge case here: Zod strips fields not listed in CredentialStateSchema. A stack-ARN-only update reads and rewrites the whole file, so it can delete future or unowned fields inside an existing credential entry. I think this nested schema should use .passthrough() too.
| targets: { ...state.targets, [targetName]: merged }, | ||
| }; | ||
|
|
||
| await json.write(statePath, next); |
There was a problem hiding this comment.
This state file should use the existing atomicWrite() helper. FsReadWriteJson.write() truncates the authoritative file directly, so an interruption or disk failure can leave malformed JSON that blocks later deploys.
There was a problem hiding this comment.
done! thanks for the thorough review!
Address review of the deployed-state work: - Validate any existing state before deploy, so a malformed file fails before AWS is mutated rather than after (leaving the new stack ARN unrecorded). The vended app likewise only treats a missing file as absent and surfaces a malformed one. - Require a stack ARN on a deploy result; a successful CDK deploy always has one, so its absence is malformed -- fail instead of silently skipping persistence. - Write the state file atomically (temp + rename) so an interruption can't leave unparseable JSON that blocks later deploys. - Passthrough the credential-entry schema so a stack-ARN-only rewrite doesn't strip fields a newer CLI records. - Qualify the merge guarantee: safe for sequential deploys, not concurrent.
|
Claude Security Review: no high-confidence findings. (run) |
Move the state file back under agentcore/.cli/ to match the released CLI's location, so a project created by an older CLI keeps reading the same path after upgrading (the vended app isn't re-vended on deploy). The scaffolded .gitignore ignores the rest of .cli/ but re-includes deployed-state.json, so the stack binding + credential ARNs stay committed and shared.
|
Claude Security Review: no high-confidence findings. (run) |
Moves the deploy-state file to a committed
agentcore/.cli/deployed-state.json, and stops storing a full snapshot of every resource.State is now keyed per target and holds only the deployed CloudFormation stack ARN (captured from the toolkit deploy result) plus the imperatively-created credential ARNs the synth step needs before the stack exists. Everything else is meant to be read live from CloudFormation, so the file can't go stale.
Changes
DeployedStateschema +readDeployedState/updateTargetState(merge-not-clobber: preserves sibling targets and keys this CLI doesn't own).stackArn;CdkBackend.deploypersists it per target after a successful deploy.Notes
statusand resource ARNs) and the credentials preflight that populates the credential ARNs are follow-ups.