Skip to content

feat(bootstrap): resource-action-map for synth-time validation - #165

Open
scottschreckengaust wants to merge 11 commits into
mainfrom
feat/bootstrap-action-map
Open

feat(bootstrap): resource-action-map for synth-time validation#165
scottschreckengaust wants to merge 11 commits into
mainfrom
feat/bootstrap-action-map

Conversation

@scottschreckengaust

Copy link
Copy Markdown
Contributor

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 on compute_type context 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

  • ECS context gate (refactor(compute): gate ECS construct on compute_type context instead of comment toggle #164): Construct is always in source, compute_type governs synthesis — no commenting/uncommenting
  • getRequiredBootstrapPolicies(computeType): Single function declaring what the app needs, consumed by Aspect and preflight
  • Dual-config synth-coverage test: Validates map completeness for both agentcore and ecs configurations
  • Map scoped to this app resources (~60 types): Unknown types produce warnings, not errors
  • All map actions within configured policy set: Test enforces the map never requires more than policies allow

Deliverables

Test plan

  • All existing CDK tests pass
  • Map covers all resource types in both synth configurations
  • All mapped actions exist in the combined policy set (wildcard-aware)
  • getRequiredBootstrapPolicies returns correct sets for each compute type
  • tsc --noEmit compiles cleanly
  • No circular imports between preflight/ and policies/

Open questions

  • SQS: AWS::SQS::Queue is in the template but no policy has SQS actions — needs investigation (may require policy update + version bump)

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

@scottschreckengaust
scottschreckengaust force-pushed the feat/bootstrap-action-map branch from d31fd4d to d3a9804 Compare May 21, 2026 07:50
@scottschreckengaust

Copy link
Copy Markdown
Contributor Author
┌─────────┬──────┬───────────────────────────────────────────┐
│ Commit  │ Task │                   What                    │
├─────────┼──────┼───────────────────────────────────────────┤
│ d3a9804 │ 0    │ ECS context gate (closes #164)            │
├─────────┼──────┼───────────────────────────────────────────┤
│ 5ed8db3 │ 1    │ getRequiredBootstrapPolicies(computeType) │
├─────────┼──────┼───────────────────────────────────────────┤
│ 83099e1 │ 2    │ Resource-action-map (57 CF types)         │
├─────────┼──────┼───────────────────────────────────────────┤
│ ed0cf6b │ 3    │ Dual-config synth-coverage test           │
└─────────┴──────┴───────────────────────────────────────────┘

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
(f46cfb7) will be skippable and the #123 commits will drop out, leaving just the 4 clean #124 commits on main.

Comment thread cdk/src/bootstrap/required-policies.ts Outdated
Comment thread cdk/src/bootstrap/required-policies.ts
Comment thread cdk/src/stacks/agent.ts
@krokoko

krokoko commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

on it

@krokoko

krokoko commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review — feat(bootstrap): resource-action-map for synth-time validation

Thanks for this — the direction is great. Converting the bootstrap permissions into a versioned, testable map advances the "bounded blast radius" tenet from RFC #120, and replacing the comment-toggle with a real compute_type context gate (#164) is a clear improvement that keeps the ECS path always-compilable. The data model is clean and the pure-function unit tests are genuinely strong.

I do think a few things should be addressed before merge. Verdict: request changes — all correctable in a focused follow-up.

Blocking

B1 — The map is missing the ECS resource types this PR enables, and the included test is red.
EcsAgentCluster synthesizes AWS::ECS::Cluster and AWS::ECS::TaskDefinition (cdk/src/constructs/ecs-agent-cluster.ts:79,108), but RESOURCE_ACTION_MAP has no AWS::ECS::* entry and neither type is in the test's SKIP_TYPES. Running cdk synth -c compute_type=ecs and then the suite fails:

Test "all ecs resource types have map entries"
  Received + ["AWS::ECS::Cluster", "AWS::ECS::TaskDefinition"]

The sibling policy compute-ecs.ts already grants the matching ecs:* actions, so the map is the only thing out of sync. Suggested fix — add both entries, deriving actions from compute-ecs.ts:

'AWS::ECS::Cluster': {
  create: ['ecs:CreateCluster', 'ecs:TagResource', 'ecs:PutClusterCapacityProviders'],
  read:   ['ecs:DescribeClusters', 'ecs:ListTagsForResource'],
  update: ['ecs:UpdateCluster', 'ecs:UpdateClusterSettings', 'ecs:PutClusterCapacityProviders', 'ecs:TagResource', 'ecs:UntagResource'],
  delete: ['ecs:DeleteCluster'],
},
'AWS::ECS::TaskDefinition': {
  create: ['ecs:RegisterTaskDefinition', 'ecs:TagResource'],
  read:   ['ecs:DescribeTaskDefinition', 'ecs:ListTaskDefinitions', 'ecs:ListTagsForResource'],
  update: ['ecs:RegisterTaskDefinition', 'ecs:TagResource', 'ecs:UntagResource'],
  delete: ['ecs:DeregisterTaskDefinition'],
},

B2 — Both "Synth coverage" tests pass/skip silently, so the map's only drift-defense isn't load-bearing. (cdk/test/bootstrap/resource-action-map.test.ts)

  • The agentcore test reads cdk.out/backgroundagent-dev.template.json; if it's missing → if (types.length === 0) return; runs zero assertions. cdk.out/ is gitignored and cdk:test depends only on :compile (not :synth), running parallel to :synth:quiet — so on a clean CI runner the template may not exist yet and the test passes vacuously.
  • The ECS test wraps the whole synth in a bare catch { …; return; } // skip gracefully, which swallows every failure mode (a real synth/cdk-nag regression, a timeout, an npx blip) and reports green. This is most likely why CI is green while the test is actually red locally — CI hits the skip path.

Suggested fix: synthesize in-process so the tests are self-contained, and add a non-vacuous guard:

expect(types.length).toBeGreaterThan(0); // fail if synth produced nothing
expect(types.filter(t => !SKIP_TYPES.has(t) && !RESOURCE_ACTION_MAP[t])).toEqual([]);

For the ECS shell-out, capture the error and throw with stderr rather than return; if a no-tooling skip is genuinely needed, gate it on an explicit env flag and use it.skip so the skip is reported, not hidden.

B3 — The compute_type gate (the actual behavioral change) has no direct test. (cdk/src/stacks/agent.ts:566-619)
Nothing asserts that default context produces no ECS resources, that compute_type=ecs produces them, or that ecsConfig is wired into TaskOrchestrator only in the ECS case. The touched assertion in github-tags.test.ts:142 was changed 'ecs''custom-compute', which (understandably, to avoid the Docker asset build) removes the sole incidental exercise of the ECS path. Suggested:

test('default → no ECS', () => { const t = synth({}); t.resourceCountIs('AWS::ECS::Cluster', 0); });
test('compute_type=ecs → ECS cluster + task def', () => {
  const t = synth({ compute_type: 'ecs' });
  t.resourceCountIs('AWS::ECS::Cluster', 1);
  t.resourceCountIs('AWS::ECS::TaskDefinition', 1);
});

plus an assertion that the orchestrator receives ecsConfig only when ecs.

Non-blocking suggestions

  • Unused for now: getRequiredBootstrapPolicies, RESOURCE_ACTION_MAP, getActionsForResource, getAllMappedActions have no production caller yet (only barrels + tests). Totally fine as staged scaffolding for the Aspect (feat(bootstrap): CDK Aspect for policy envelope checking #125) / preflight (feat(bootstrap): live-account preflight validator #126) — just worth stating explicitly in the description so reviewers don't expect synth-time enforcement yet.
  • Untracked gaps: KNOWN_GAP_SERVICES/KNOWN_GAP_ACTIONS (sqs:*, s3:CreateBucket+lifecycle, Lambda EventSourceMapping/LayerVersion) are genuine — the stack creates those resources but no policy grants the actions, so the "all mapped actions exist in policies" test passes only by excluding the cases it most needs to catch. Could each be tied to a tracking issue (// gap tracked in #NNN), noted in DEPLOYMENT_ROLES.md, and guarded with a "gap set only shrinks" assertion?
  • Test polish: temp-dir rmSync is duplicated rather than in a finally (:233,241); a couple of throws after an expect().toBe(true) that already aborts are dead (:101-104,:116-119); getActionsForResource only asserts create/delete, never read/update.
  • The ...(ecsCluster && { ecsConfig: {...} }) spread in agent.ts:596 is correct (ecsConfig is optional and spreading a falsy is a no-op) — flagging only because it's an unusual pattern.

Docs / security

No docs changes needed for this internal scaffolding, and the Starlight mirror sync isn't triggered (no docs//CONTRIBUTING.md edits). The map is inert until #125/#126 consume it, so there's no new IAM/network surface at deploy time — but when the Aspect lands, please make sure the KNOWN_GAP_* exclusions don't translate into under- or over-scoped roles.

Really nice foundation overall — just want the coverage tests to actually fail when they should, and the ECS entries added, before this goes in. 🙏

Review assisted by Claude Code (code-reviewer, silent-failure-hunter, pr-test-analyzer agents).

@scottschreckengaust

scottschreckengaust commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Question for reviewers:

Option 1:
Keep the stacked PRs and continue with incremental changes to the main.

Option 2:
Start from the end of the stack with a large PR to be merged to main at the end.

scottschreckengaust and others added 8 commits August 6, 2026 02:38
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>
@scottschreckengaust
scottschreckengaust force-pushed the feat/bootstrap-action-map branch from ae4858d to f782707 Compare August 6, 2026 03:28
@scottschreckengaust
scottschreckengaust requested a review from a team as a code owner August 6, 2026 03:28
@scottschreckengaust

Copy link
Copy Markdown
Contributor Author

@krokoko Picking this back up — rebased onto main (d3974f7a) and worked your blocking review. Answering your Option 1/Option 2 question from 2026-06-08 with Option 1 (incremental to main): the six reactive permission fixes that landed while this sat (#351, #403, #405, #408, #410, #494, #595) are the evidence that deferring costs more than it saves.

One structural decision that changes where the fixes land. While this PR was idle, main grew its own cdk/src/bootstrap/resource-action-map.ts reactively via #351 (issue #350) — outside the RFC — and that is the map wired into the live gate (synth-coverage.test.ts). This branch's bootstrap/preflight/ copy has no production consumer. Fixing the unreachable one would have left the real gate blind, so B1/B2 are fixed on main's live map. preflight/ stays as the verification/testing layer per @scottschreckengaust.

B1 — ECS blind spot: fixed

Confirmed with a real gated synth: --context compute_type=ecs emits exactly AWS::ECS::Cluster + AWS::ECS::TaskDefinition as unmapped, so compute-ecs.ts's 14 ecs:* grants were unverified. Both added to the live map, actions derived from compute-ecs.ts as you suggested.

B2 — vacuous tests: fixed, and I confirmed your diagnosis

You were right that CI was hitting the skip path. The catch { … return } swallowed a nonzero synth exit — locally npx cdk synth -c compute_type=ecs exits 1 (an ec2:DescribeAvailabilityZones denial in my account), so the test burned ~86s, reported green, and asserted nothing.

Rather than repair the shell-out, I took your "synthesize in-process" suggestion into synth-coverage.test.ts — no child process, so there is no exit code to swallow — plus the non-vacuous guard you asked for:

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:

mutation result
remove the two ECS map entries ✅ fails — reports both as unmapped
hard-code computeType = 'agentcore' (silently broken gate) ✅ fails the toContain guard

Both passed before the fix.

B3 — already satisfied by #596

agent.test.ts:674 now has the ECS-gate describe block you specified: cluster + both task-defs, the ComputeSubstrate output, and the default no-gate case. No new work needed; #164 is delivered.

Non-blocking items

  • getRequiredBootstrapPolicies now has a production caller. Your note that it was inert was fair. collectBootstrapAllowActions() called allPolicies() unconditionally — validating against the union — so RFC RFC: Least-privilege CDK bootstrap policies as code (with preflight validation) #120's deployed ⊇ required model was unimplemented and an agentcore-only operator's app was checked against ecs:* grants their IaCRole cannot perform. Added policiesForComputeType(), routed through the salvaged function (fails loud on an unregistered name). Measured: union 357 actions / 14 ecs:*; agentcore scope 343 / 0 ecs:*; ecs scope 356 / 14 ecs:* / 0 bedrock-agentcore:*. The argument is optional, so the union behaviour is preserved for callers that want it.
  • KNOWN_GAP_* — closed by granting, not excluding. You flagged these as "genuine gaps … the test passes only by excluding the cases it most needs to catch." Granted all four: s3:GetBucketPolicy + s3:GetEncryptionConfiguration (CFN reads both back on stack update; every other Put* in that statement already had its Get* pair) and sqs:AddPermission + sqs:RemovePermission (AWS::SQS::QueuePolicy is managed via Add/RemovePermission, not SetQueueAttributes, and the stack creates one for DLQ redrive). BOOTSTRAP_VERSION → 1.3.0, artifacts regenerated, DEPLOYMENT_ROLES.md updated for golden parity, Starlight mirror re-synced.

One thing found on the way — filed, not fixed here

BOOTSTRAP_HASH is blind to every policy action (#732). I added four IAM actions and the committed hash was byte-identical. computeBootstrapHash passes Object.keys(json).sort() as JSON.stringify's second argument, which is a replacer allowlist, not a sort — so Statement array elements serialize to {} and only statement counts are protected. Swapping an action, or widening a resource ARN to *, leaves the digest untouched. Pre-existing on main from #122; left out of this PR to keep it reviewable, but #125/#126 will depend on that digest meaning something.

Still to come on this PR

Deepening the live map to full CRUD and retiring the duplicate data (keeping preflight/ as a facade). That is where the real value of the 428-line map is: it carries 48 create-phase actions the flat map lacks on shared types, and Update*/Tag*/Delete* depth is precisely what the six reactive fixes kept rediscovering.

178 suites / 3533 tests pass.

scottschreckengaust and others added 2 commits August 6, 2026 04:34
… 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>
@scottschreckengaust

Copy link
Copy Markdown
Contributor Author

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 path

Merged programmatically, not by hand, with the safety invariant asserted mechanically: every action from the create-only map must survive in the merged entry's create phase. Verified 0 lost across 52 types.

before after
live map 52 types, create-only 64 types, 430 actions, CRUD
preflight/resource-action-map.ts 428 lines of duplicate data 62-line facade, no data

The CRUD map contributed 48 create-phase actions the live map lacked on shared types, plus AWS::IAM::ManagedPolicy; main's 6 extra types (CloudFront, Custom::*, CDK::Metadata) are preserved. preflight/ stays as the verification layer per @scottschreckengaust — it now re-exports the single map and keeps the query helpers #125/#126 will read it through, so there is no second copy to drift.

findMissingBootstrapActions defaults to ['create'], preserving the pre-CRUD contract for existing callers; pass phases to widen.

KNOWN_GAP_* removed entirely — the gate now has zero exemptions

This is the part I want to flag, because the exclusions turned out to be broader than the problem. I checked all 11 excluded actions against the policies: every one was already covered. The sqs and s3 entries excluded whole services, and all 7 lambda actions were stale. They were hiding nothing.

With the 4 genuine gaps closed by granting (previous commit), the coverage assertion now runs over all 430 actions in all four phases with no exemptions — which is what you 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.

Also deleted the two vacuous Synth coverage tests from this file — both bailed silently and the ECS one burned ~86s asserting nothing. synth-coverage.test.ts now covers both configs in-process and fails loudly.

Structural pins so the depth can't erode

Two new tests: every entry must declare all four phases as arrays (a regression to readonly string[] would make actionsForResource silently skip phases), and ≥45 entries must carry real update/delete actions.

One bug found and fixed along the way

The new in-process ECS synth builds the agent DockerImageAsset, which fingerprints the repo root — and Jest workers evict .jest-cache/jest-transform-cache-* entries mid-run, so the walk can hit ENOENT on a path another worker just deleted. It surfaced once in a full mise run build and would not reproduce across three cold-cache runs, so I fixed it structurally rather than retrying.

.dockerignore already documents this exact vanishing-file class for pytest-cov's .coverage.* temp files. .jest-cache was in .gitignore but not .dockerignore, and .dockerignore is what CDK's fingerprint honours. Verified by asserting no staged asset directory contains cdk/.jest-cache.

Final state

cdk 178 suites / 3533 tests · cli 55 / 695 · agent 1460 · //cdk:eslint clean, no mutations.

//cdk:synth:quiet fails locally on ec2:DescribeAvailabilityZones — an IAM gap in my sandbox account, reproduced identically on a near-main branch, unrelated to this PR.

All of krokoko's blockers are addressed: B1 (ECS entries on the live map, mutation-tested), B2 (loud in-process coverage, mutation-tested both ways), B3 (already delivered by #596). Non-blocking items: getRequiredBootstrapPolicies has a production caller, KNOWN_GAP_* closed by granting, CRUD depth merged and enforced. Ready for re-review.

Still open and deliberately out of scope: #732 (BOOTSTRAP_HASH is blind to every policy action — pre-existing from #122, and #125/#126 will need it to mean something).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants