feat(bootstrap): resource-action-map for synth-time validation - #165
feat(bootstrap): resource-action-map for synth-time validation#165scottschreckengaust wants to merge 11 commits into
Conversation
d31fd4d to
d3a9804
Compare
Note: this branch currently sits on top of feat/bootstrap-template (#162). When #162 merges to main, I'll retarget and rebase per ADR-001 §8 — the scaffold commit |
ed0cf6b to
ab01560
Compare
d42d870 to
c5b7401
Compare
8a27b84 to
5bc41d6
Compare
a3fcb8f to
684817e
Compare
|
on it |
Review —
|
|
Question for reviewers: Option 1: Option 2: |
Replace comment toggle with proper context gate. ECS resources only synthesize when compute_type=ecs is passed. Default (agentcore) behavior unchanged. Closes #164 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…are policy selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Maps all CloudFormation resource types used by the ABCA stack to their required IAM actions per lifecycle phase (create/read/update/delete). Actions are sourced from CloudTrail-validated policies in DEPLOYMENT_ROLES.md. Tests validate structure, format, and policy coverage (with known gaps for SQS, S3 bucket lifecycle, and Lambda ESM/Layer actions documented). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validates that all resource types in the synthesized CloudFormation template have entries in the resource-action-map. Tests agentcore from existing cdk.out and attempts ECS synth gracefully skipping when AWS credentials are unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
compute_type drives which compute policy is needed — agentcore and ecs are independent choices, not base+optional. An operator deploying only ECS should not require agentcore permissions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The resource-action-map test previously synthesized into cdk/cdk.out.ecs/ inside the repo tree. CDK's AgentRuntimeArtifact.fromAsset(repoRoot) fingerprints the entire tree, so when github-tags.test runs in parallel it can stat synth.lock mid-lifecycle and hit ENOENT. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e selection (#124) Addresses krokoko's blocking review on #165. The work lands on main's LIVE map (cdk/src/bootstrap/resource-action-map.ts, consumed by synth-coverage.test.ts) rather than the branch's parallel bootstrap/preflight/ copy — main grew its own map via #351 while this PR sat, and fixing the unreachable one would leave the real gate blind. B1 — ECS was a total validation blind spot. Neither map had any AWS::ECS::* entry, so compute-ecs.ts's 14 `ecs:*` grants were unverified. Confirmed against a real gated synth: `--context compute_type=ecs` emits exactly AWS::ECS::Cluster + AWS::ECS::TaskDefinition as unmapped. Both added, actions derived from compute-ecs.ts. B2 — the dual-config coverage check was vacuous. The original shelled out to `npx cdk synth` and swallowed every failure (`catch { return }`), plus bailed on `types.length === 0` — it burned ~86s, reported green, and asserted nothing. Replaced with an IN-PROCESS ECS-gated synth in synth-coverage.test.ts (no child process, so no try/catch to swallow), and an explicit toContain guard on the two ECS types so the check cannot pass vacuously if the gate ever stops provisioning. Mutation-tested both directions: removing the ECS map entries fails with the 2 unmapped types; hard-coding computeType to 'agentcore' (a silently broken gate) fails the toContain guard. The pre-fix version passed under both. Compute-type-aware selection — RFC #120's sufficiency model is `deployed PolicySet ⊇ the app's required set`, but collectBootstrapAllowActions called allPolicies() unconditionally, validating against the UNION of all five. An agentcore-only operator never deploys compute-ecs, so the union silently accepts `ecs:*` their real IaCRole cannot perform — the over-permissive direction. Added policiesForComputeType(), routed through the salvaged getRequiredBootstrapPolicies so selection cannot drift from the generated artifacts (fails loud on an unregistered name), and made the computeType argument OPTIONAL so the historical union behaviour is preserved for callers that want "grantable by some configuration". Verified scoping: union 357 actions (14 ecs:*), agentcore scope 343 (0 ecs:*), ecs scope 356 (14 ecs:*, 0 bedrock-agentcore:*). 178 suites / 3533 tests pass. B3 needs no work: #596 already landed the ECS-gate tests krokoko asked for (agent.test.ts:674 — cluster + both task-defs, ComputeSubstrate output, and the default no-gate case). Co-Authored-By: Claude <noreply@anthropic.com>
krokoko's non-blocking review point: the KNOWN_GAP_SERVICES/KNOWN_GAP_ACTIONS
exclusions were "genuine gaps — the stack creates those resources but no policy
grants the actions, so the test passes only by excluding the cases it most needs
to catch." Closing them by granting, rather than by keeping the exclusion.
- s3:GetBucketPolicy, s3:GetEncryptionConfiguration (observability, S3Application
Buckets). CloudFormation reads a bucket's policy and encryption config back on
stack UPDATE for drift/no-op detection, so the existing Put* grants are
insufficient alone. Every other Put* in that statement already had its Get*
pair; these two were the omissions.
- sqs:AddPermission, sqs:RemovePermission (application, SQS). AWS::SQS::Queue
Policy is a distinct CFN resource managed via Add/RemovePermission, NOT
SetQueueAttributes. The stack creates one (the DLQ redrive policy), so a
queue-policy create/update/delete would fail.
Verified all four resolve through actionIsAllowed after the change.
BOOTSTRAP_VERSION 1.2.0 -> 1.3.0 (additive grants, backward-compatible) with
artifacts regenerated via //cdk:bootstrap:generate, DEPLOYMENT_ROLES.md updated
to keep golden-baseline parity, and the Starlight mirror re-synced.
NOTE: BOOTSTRAP_HASH is byte-identical after adding four IAM actions, which is
wrong — computeBootstrapHash misuses JSON.stringify's replacer argument as a key
sort, so it digests `{}` for every statement and is blind to all actions.
Pre-existing on main (introduced with the hash in #122), filed as #732 rather
than fixed here to keep this PR reviewable.
178 suites / 3533 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
ae4858d to
f782707
Compare
|
@krokoko Picking this back up — rebased onto One structural decision that changes where the fixes land. While this PR was idle, B1 — ECS blind spot: fixedConfirmed with a real gated synth: B2 — vacuous tests: fixed, and I confirmed your diagnosisYou were right that CI was hitting the skip path. The Rather than repair the shell-out, I took your "synthesize in-process" suggestion into expect([...typesInTemplate]).toContain('AWS::ECS::Cluster');
expect([...typesInTemplate]).toContain('AWS::ECS::TaskDefinition');Mutation-tested both directions, since a coverage test that cannot fail is the whole defect:
Both passed before the fix. B3 — already satisfied by #596
Non-blocking items
One thing found on the way — filed, not fixed here
Still to come on this PRDeepening the live map to full CRUD and retiring the duplicate data (keeping 178 suites / 3533 tests pass. |
… a facade (#124) Retires the duplicate map. Two copies existed: bootstrap/preflight/ carried CRUD depth with no production consumer, while bootstrap/resource-action-map.ts was create-only and wired into the live synth-coverage gate. Disjoint test suites and no shared consumer means they drift by construction, and adding a resource type to only one of them is silent. Merged programmatically, not by hand, with the invariant asserted mechanically: every action from the create-only map survives in the merged entry's `create` phase (verified 0 lost across 52 types). Result is 64 types / 430 actions, up from 52 create-only entries — the CRUD map contributed 48 create-phase actions the live map lacked on shared types, plus AWS::IAM::ManagedPolicy, while main's 6 extra types (CloudFront, Custom::*, CDK::Metadata) are preserved. - RESOURCE_ACTION_MAP is now Record<string, ResourceActions> with create/read/update/delete. findMissingBootstrapActions defaults to ['create'], preserving the pre-CRUD contract for existing callers; pass phases to widen. - bootstrap/preflight/resource-action-map.ts holds NO data — it re-exports the single map and keeps the query helpers (getActionsForResource, getAllMappedActions) that #125/#126 will read it through. 428 lines -> 62. - Deleted the two vacuous 'Synth coverage' tests here: both bailed silently (`catch { return }`, `types.length === 0`) and the ECS one burned ~86s asserting nothing. synth-coverage.test.ts now covers both configs in-process and fails loudly (previous commit). KNOWN_GAP_SERVICES / KNOWN_GAP_ACTIONS removed entirely. Verified every one of the 11 excluded actions is now covered — the sqs/s3 service-wide exclusions and all 7 lambda actions were stale, hiding nothing. With the 4 real gaps granted in the previous commit, the coverage assertion runs over ALL 430 actions in ALL four phases with zero exemptions, which is what krokoko asked for ("the test passes only by excluding the cases it most needs to catch"). Mutation-tested: revoking sqs:AddPermission fails with "1 actions not covered by bootstrap policies: sqs:AddPermission". Added structural pins so the depth cannot erode: every entry must declare all four phases as arrays, and >=45 entries must carry real update/delete actions. 178 suites / 3533 tests pass; //cdk:eslint clean, no mutations. Co-Authored-By: Claude <noreply@anthropic.com>
The new in-process ECS synth-coverage test builds the agent DockerImageAsset, which fingerprints the whole repo root. Jest workers create and evict `.jest-cache/jest-transform-cache-*/<n>/<name>_<hash>.map.<random>` entries throughout a run, so the fingerprint walk can hit a path another worker just deleted: Resolution error: ENOENT: no such file or directory, open '.../cdk/.jest-cache/jest-transform-cache-.../80/denytasktest_....map.588130630' Intermittent — it surfaced once in a full `mise run build` and did not reproduce across three cold-cache runs, which is exactly why it needs a structural fix rather than a retry. This is the same vanishing-file class .dockerignore already documents for pytest-cov's `.coverage.<host>.<pid>.<random>` temp files, with the same consequence. `.jest-cache` was in .gitignore but not .dockerignore, and .dockerignore is what CDK's fingerprint honours. Verified by synthesizing the stack and asserting no staged asset directory contains `cdk/.jest-cache` (5 asset dirs, none leaked). Full build now passes cdk 178 suites / 3533 tests and cli 55 / 695. Note: `//cdk:synth:quiet` still fails locally on `ec2:DescribeAvailabilityZones` — an IAM gap in my sandbox account, reproduced identically on a near-main branch, unrelated to this change. Co-Authored-By: Claude <noreply@anthropic.com>
|
Follow-up to the previous comment — the remaining work is done. The duplicate map is retired and the CRUD depth is now enforced rather than decorative. One map, on the live pathMerged programmatically, not by hand, with the safety invariant asserted mechanically: every action from the create-only map must survive in the merged entry's
The CRUD map contributed 48 create-phase actions the live map lacked on shared types, plus
|
Summary
Closes #124
Closes #164
Creates a mapping from CloudFormation resource types to required IAM actions (CRUD lifecycle), scoped to all resource types in this app's synthesized template. Introduces
getRequiredBootstrapPolicies()for downstream consumption by the Aspect (#125) and preflight validator (#126). Gates ECS construct oncompute_typecontext variable (replaces comment toggle).Stack position
PR 5 for #120 — least-privilege CDK bootstrap policies as code
Prior: Custom template generator + compute variants (PR #162, #123)
This PR: Resource-action-map + ECS context gate + required-policies module
Next: CDK Aspect for policy envelope checking (#125)
Key decisions
Deliverables
Test plan
Open questions
Implementation plan
See: docs/superpowers/plans/2026-05-21-resource-action-map.md
Blocked by: #123 (PR #162)
References: RFC #120, ADR-002
🤖 Generated with Claude Code