Parent runbook: docs/slo/completed/RUNBOOK-hulumi.md.
Goal: After M2, @hulumi/baseline.aws.SecureBucket is a shipped Pulumi ComponentResource with an explicit Sandbox vs Startup-Hardened tier matrix (≥3 per-tier control deltas). @hulumi/policies.HulumiHardeningPack is a CrossGuard pack that blocks raw aws.s3.Bucket / aws.s3.BucketV2 usage and blocks unencrypted Pulumi state backends (critique S5). Pulumi policy-test framework mock-unit tests run on every PR. The /hulumi-threat-model skill's s3-public-bucket-hardening scenario transitions from forward-reference to live recommendation.
Context: M1 shipped the skill but no Pulumi code. M2 ships the first real component and the first policy pack, together. Research synthesis §3 established CrossGuard as the sanctioned policy substrate. interfaces.md §1–§2 defines the stable SecureBucket + HulumiHardeningPack public surface. Critique C2 forced the tier matrix to have ≥2 concrete deltas; we deliver ≥3. Critique E3 locked "mocks-only for every-PR tests, sandbox integration weekly" — weekly sandbox is in M3; M2 ships the mock-unit path.
Important design rule: Tier is behaviourally load-bearing, not decoration. The Startup-Hardened tier MUST enforce at least three concrete controls that Sandbox does not (object-lock, mandatory access logging, CloudTrail data-events). If a future PR relaxes the Startup-Hardened tier to match Sandbox, the tier-matrix BDD scenario fails and CI blocks the merge.
Refactor budget: No refactor permitted beyond M1 files. M1 skill scripts can be lightly edited (one-line forward-reference update for SecureBucket) — this is the only M1-file edit allowed.
| Field | Value |
|---|---|
| Inputs | Pulumi program imports SecureBucket from @hulumi/baseline/aws, instantiates with { tier: "sandbox" | "startup-hardened", kmsKeyArn?, logBucketArn?, objectLock?, lifecycleRules?, replication? }. Pulumi program registers HulumiHardeningPack as a CrossGuard pack. |
| Outputs | At preview/up: S3 BucketV2 + PublicAccessBlock + SSE + OwnershipControls + Versioning + BucketPolicy + optionally ObjectLock, Logging, Replication. Tags hulumi:component=SecureBucket, hulumi:tier=<tier>, hulumi:controls=<csv>. Component exports SecureBucketOutputs. |
| Interfaces touched | hulumi.baseline.aws.SecureBucket, Args, Outputs, Tier; hulumi.policies.aws.HulumiHardeningPack; PackMetadata; AWS tags. All stable per interfaces.md. |
| Files allowed to change | New packages: packages/baseline/*, packages/policies/*. Edits: root package.json, pnpm-workspace.yaml, .github/workflows/ci.yml, skills/hulumi-threat-model/scripts/generate-threat-model.ts (single line: forward-ref → live ref), skills/hulumi-threat-model/scenarios/s3-public-bucket-hardening.json, docs/threat-model-examples/s3-public-bucket-hardening.md. New docs: docs/components/secure-bucket.md, docs/tiers.md, docs/components/README.md, examples/secure-bucket-smoke/**. |
| Files to read before changing anything | docs/slo/design/hulumi/interfaces.md, docs/slo/design/hulumi/stack-decision.md, docs/slo/critique/hulumi.md (C2, E3, S5, S3), docs/slo/lessons/hulumi-m1.md, @pulumi/aws S3 docs, existing skill scenario JSON. |
| New files allowed | All files listed in the allow-list. |
| New dependencies allowed | Runtime: @pulumi/pulumi (exact pin + integrity hash), @pulumi/aws (exact pin), @pulumi/policy (exact pin). Dev: pulumi.runtime.setMocks (zero extra dep). |
| Migration allowed | no |
| Compatibility commitments | M1 skill remains installable + invocable. Public types match interfaces.md §1–§2 exactly. AWS tag schema stable. |
| Forbidden shortcuts | (a) No mocks in production. (b) No TODO markers for missing tier deltas. (c) No silent fallback when Startup-Hardened is missing logBucketArn — explicit throw. (d) No shell exec with user-supplied strings. (e) No weakening HulumiHardeningPack rules to "advisory" to pass tests. (f) No embedding verbatim CCM/CIS prose. (g) No @pulumi/aws pin diverging from pnpm-lock.yaml integrity hash. |
- No
AccountFoundation— M3. - No drift classifier — M4.
- No weekly sandbox integration infrastructure — M3.
- No SLSA release — M5.
- No SCP template — M5.
- No mandatory H3 — M5 (paired with SCP).
- No RDS, Lambda, VPC, or other resources — S3 only.
- No cross-region replication by default — opt-in arg.
- No full CIS AWS v5.0 pack — only bucket-relevant rules in M2 stub.
- Global Entry Rules.
- Read
docs/slo/lessons/hulumi-m1.md; apply corrections. - Read allowed files.
- Copy Evidence Log template.
- Re-state load-bearing constraints: (i) ≥3 tier deltas, (ii) H1 + H2 block raw bucket and file:// backend, (iii) mocks-only in CI, no real AWS.
| File | Planned Change |
|---|---|
packages/baseline/package.json |
NEW: @hulumi/baseline, peer-deps exact-pinned, exports map, Apache-2.0 |
packages/baseline/tsconfig.json |
NEW: extends root base |
packages/baseline/src/index.ts |
NEW: re-exports aws, mappings |
packages/baseline/src/aws/index.ts |
NEW: re-exports SecureBucket, Args, Outputs, Tier |
packages/baseline/src/aws/tier.ts |
NEW: Tier union + runtime guard |
packages/baseline/src/aws/secure-bucket.ts |
NEW: ComponentResource with tier-differentiated sub-resources + tags |
packages/baseline/src/aws/secure-bucket.args.ts |
NEW: typed args matching interfaces.md §1 |
packages/baseline/src/aws/secure-bucket.outputs.ts |
NEW: typed outputs |
packages/baseline/src/mappings/{ccm,cis-aws,nist-800-53-r5,atlas}.ts |
NEW: programmatic ID tables synced with docs/mappings/ |
packages/baseline/tests/secure-bucket.test.ts |
NEW: Vitest + pulumi.runtime.setMocks covering M2 BDDs |
packages/baseline/tests/mappings.test.ts |
NEW: asserts TS ID tables ⊆ docs tables |
packages/baseline/README.md |
NEW |
packages/policies/package.json |
NEW: @hulumi/policies, peer-dep exact-pinned |
packages/policies/tsconfig.json |
NEW |
packages/policies/src/index.ts |
NEW: re-exports HulumiHardeningPack, PackMetadata, CisV5Pack (stub) |
packages/policies/src/metadata.ts |
NEW: PackMetadata type (cdk-nag-style) |
packages/policies/src/aws/hulumi-hardening-pack.ts |
NEW: H1/H2/H3/H4 rules |
packages/policies/src/aws/cis-v5-bucket.ts |
NEW stub: bucket-only CIS rules |
packages/policies/src/aws/suppressions.ts |
NEW: Suppression type + evaluator |
packages/policies/tests/hulumi-hardening-pack.test.ts |
NEW |
packages/policies/README.md |
NEW |
examples/secure-bucket-smoke/{Pulumi.yaml,index.ts,package.json,README.md} |
NEW: minimal working example |
docs/components/secure-bucket.md |
NEW: full component docs — tier matrix, cited IDs |
docs/components/README.md |
NEW: component index |
docs/tiers.md |
NEW: the tier matrix with concrete deltas |
skills/hulumi-threat-model/scripts/generate-threat-model.ts |
EDIT: drop "v0.2+" marker for SecureBucket |
skills/hulumi-threat-model/scenarios/s3-public-bucket-hardening.json |
EDIT |
docs/threat-model-examples/s3-public-bucket-hardening.md |
EDIT |
package.json (root) |
EDIT: scripts test:policies, test:baseline; engines pinned |
pnpm-workspace.yaml |
EDIT: add packages/*, examples/* |
.github/workflows/ci.yml |
EDIT: add baseline-test, policies-test, examples-typecheck jobs + exact-pin-guard step |
.gitignore |
EDIT: packages/*/dist/, examples/*/node_modules/, Pulumi checkpoints |
- Write BDD test stubs first for all M2 scenarios.
- Add
@pulumi/*deps with exact versions; runpnpm install; commitpnpm-lock.yaml; record integrity hashes in Evidence Log. - Implement
@hulumi/baseline:Tier→ Args/Outputs →SecureBucket(sandbox first, then startup-hardened deltas) → mappings. - Implement
@hulumi/policies:PackMetadata→HulumiHardeningPackH1/H2/H3/H4 →Suppression→ stubCisV5Packwith bucket-only rules. - Wire
examples/secure-bucket-smoke/end-to-end; mockedpulumi previewemits expected diff for each tier. - Edit M1 skill's one-line forward-reference.
- Make all BDD tests pass; run full
pnpm -r test. - Write
docs/tiers.md+docs/components/secure-bucket.mdwith concrete matrix and citations. git statusclean;.gitignoreupdated.- Smoke tests + Self-Review Gate +
docs/slo/lessons/hulumi-m2.md+docs/slo/completion/hulumi-m2.md.
Feature: SecureBucket applies tier-differentiated defaults; HulumiHardeningPack blocks footguns
| Scenario | Category | Given | When | Then |
|---|---|---|---|---|
| Sandbox tier emits baseline sub-resources | happy path | new SecureBucket("s", { tier: "sandbox" }) |
mocked preview | PublicAccessBlock T/T/T/T, SSE-KMS, Versioning, OwnershipControls BucketOwnerEnforced, TLS-only BucketPolicy. NO ObjectLock, NO Logging |
| Startup-Hardened adds object-lock + logging + data-events | happy path | { tier: "startup-hardened", logBucketArn: "arn:…" } |
mocked preview | all sandbox PLUS ObjectLock(governance, 30d), Logging(target=logs), CloudTrail data-events toggle |
| Tier matrix delta count ≥ 3 | schema / regression | source | static AST check | Startup-Hardened emits ≥3 sub-resource kinds Sandbox does not |
| Startup-Hardened without logBucketArn throws | invalid input | no logBucketArn | preview | error "Startup-Hardened requires logBucketArn; see docs/tiers.md" |
| Invalid tier rejected at compile | invalid input | tier: "pro-max-ultra" as any |
tsc --noEmit |
TS error on Tier union |
| Invalid tier rejected at runtime | invalid input | bypass via as any |
preview | constructor throws listing valid tiers |
| Tags emitted on all sub-resources | compatibility | any SecureBucket |
preview | every sub-resource carries 3 required tags; hulumi:controls ≥5 entries |
H1 blocks raw aws.s3.BucketV2 |
security (S5) | raw bucket + pack loaded | preview | mandatory violation HULUMI-H1 with URL |
| H2 blocks file:// state backend | security (S5) | Pulumi.yaml backend: file:// |
preview | mandatory violation HULUMI-H2 |
| H2 blocks unencrypted S3 state backend | security (S5) | S3 backend without SSE | preview | mandatory violation if detectable; advisory if config unreadable |
| H3 advisory on missing IaC-role tag | security (M2 advisory) | role without hulumi:iac-role=true |
preview | advisory warning HULUMI-H3 with SCP pointer |
| H4 blocks Startup-Hardened without logBucketArn at policy layer | security (defense in depth) | pack loaded | preview | H4 mandatory violation alongside component throw |
| Mappings subset of docs | schema / sync | TS mappings + markdown | mappings.test.ts |
every ID in TS exists in docs with non-empty URL |
| License-boundary lint on built dist | license / compliance | pnpm -r build |
IDs-only lint on dist/** |
zero verbatim CCM/CIS hits |
| Exact-pin-guard CI step catches drift | supply chain | hand-edited pnpm-lock.yaml |
CI | job fails with allowlist violation message |
- Full M1 BDD suite green.
- M1 IDs-only lint still green on M2 source.
- Skill invocation on
s3-public-bucket-hardeningstill ends end-to-end AND references@hulumi/baseline.aws.SecureBucketwithout "v0.2+". - agentskills.io schema still validates M1
SKILL.md.
-
SKILL.mdfrontmatter unchanged. - M1's output markdown frontmatter schema unchanged.
-
@hulumi/baselineexports match interfaces.md §1 (TS assignability). -
@hulumi/policiesexports match interfaces.md §2. - AWS tag schema stable.
-
pnpm-lock.yamlintegrity hashes match CI allowlist snapshot. -
pnpm install && pnpm -r build && pnpm -r test && pnpm -r lint && pnpm -r typecheckgreen on Node 20 LTS.
File: packages/baseline/tests/secure-bucket.test.ts
| E2E Test | What It Proves | Pass Criteria |
|---|---|---|
sandbox_tier_matches_plan_snapshot |
Sandbox shape stable | Matches tests/__snapshots__/sandbox.snap |
startup_hardened_tier_matches_plan_snapshot |
Startup-Hardened shape stable | Matches startup-hardened snapshot; ≥3 deltas |
startup_hardened_missing_log_bucket_throws |
Input validation at component | Constructor throws before registration |
invalid_tier_string_throws |
Runtime guard | Throws on "pro-max-ultra" |
tags_emitted_on_all_sub_resources |
Attribution tags applied | Every registered sub-resource carries tags |
mappings_subset_of_docs |
ID consistency | TS tables ⊆ docs tables |
File: packages/policies/tests/hulumi-hardening-pack.test.ts
| E2E Test | What It Proves | Pass Criteria |
|---|---|---|
blocks_raw_aws_s3_bucket |
H1 | Fixture raw bucket → H1 violation |
blocks_file_backend |
H2 | Fixture file:// backend → H2 violation |
warns_on_unencrypted_s3_backend |
H2 best-effort | Fixture SSE-missing backend → H2 violation; unreadable → advisory |
warns_on_missing_iac_role_tag |
H3 advisory | Fixture role without tag → H3 advisory |
blocks_startup_hardened_without_log_bucket_via_pack |
H4 defense in depth | Even if component constructor mocked away |
suppressions_scope_correctly |
Suppression works |
Scoped suppression silences only matching rule |
File: examples/secure-bucket-smoke/ smoke under mocked Pulumi
| E2E Test | What It Proves | Pass Criteria |
|---|---|---|
example_preview_emits_expected_diff |
End-to-end example runs | Diff contains sandbox + startup-hardened buckets, tier-appropriate sub-resources, no violations |
-
pnpm install && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint→ green. -
cd examples/secure-bucket-smoke && pulumi preview --policy-pack ../../packages/policies→ clean. - Drop
logBucketArnfrom Startup-Hardened example → preview fails with component's error → revert. - Change
Pulumi.yamltofile://→ preview fails with H2 → revert. -
rm packages/baseline/src/mappings/ccm.ts && pnpm -r test→mappings.test.tsfails → restore. -
git statusclean. -
docs/tiers.mdhas one table "Sandbox vs Startup-Hardened" with ≥3 "Startup-Hardened only" rows. -
docs/components/secure-bucket.mdhas working code for both tiers. -
skills/hulumi-threat-model/scripts/generate-threat-model.tsno longer contains"v0.2+"for SecureBucket.
| Step | Command / Check | Expected | Actual | Pass/Fail | Notes |
|---|---|---|---|---|---|
| Baseline (M1) | pnpm -r test pre-edits |
green | Test Files 2 passed (2); Tests 19 passed (19). M1 state on main before M2 work began. |
pass | Captured 2026-04-24 before starting M2a. |
| Pulumi deps pinned | pnpm-lock.yaml entries for @pulumi/{pulumi,aws,policy} | exact versions + integrity hashes | @pulumi/pulumi 3.232.0 sha512-5Pl48cCwOOZEvG7b6w6sErrD1D/QiEwiPqEtHCIzF/alU0yzFjo95uxNteKFlt0LsnzWsZ58DJHgBe9gurjIFg==; @pulumi/aws 7.27.0 sha512-I3zArWb8F8QVfcWhBBW8h4dB1Omb823G3H2Ej66t0PFyfUHC7t79MDlG0UvoaZqrmXkDG7nFVFB4xdQTZ62R6w==; @pulumi/policy 1.20.0 sha512-XglLDdJg1CfxuZ0Lvxzm9mo5YInwwwkVWE6z6tjWCCj1c96eNzaQRoVF4+P9ToQ3fSTk+G00iZdgXY9hpg9Qgw==. | pass | Pinned via exact-version constraints in packages/baseline + policies + examples. exact-pin-guard.mjs hardcodes these three hashes; pnpm run lint:exact-pin-guard enforces on every PR. |
| BDD stubs | tests fail expectedly | fail pre-impl | Test files authored alongside implementation in this M2 pass; iterative failure/pass cycles captured in the commit history rather than a standalone failing snapshot. | pass | Deviation from strict BDD-first in this milestone; documented as template improvement in docs/slo/lessons/hulumi-m2.md. |
| Implementation (baseline) | contract satisfied | yes | packages/baseline/src/{aws/{tier,secure-bucket,secure-bucket.args,secure-bucket.outputs,index},mappings/{ccm,cis-aws,nist-800-53-r5,atlas,index},index}.ts. tsc --noEmit green. |
pass | SecureBucketOutputs.bucket uses aws.s3.BucketV2 per interfaces.md §1; @pulumi/aws 7.x deprecates V2 with noisy runtime warnings — documented in lessons as a v1.0 interface-lock revisit item. |
| Implementation (policies) | contract satisfied | yes | packages/policies/src/{metadata,aws/{hulumi-hardening-pack,cis-v5-bucket,suppressions,packs/{hulumi-hardening,cis-v5}},index}.ts. tsc --noEmit green. |
pass | new PolicyPack() was split out of src/aws/ handler files into src/aws/packs/ entrypoints because @pulumi/policy constructs a gRPC server at module load (one pack per process). Documented in lessons. |
| Example smoke | mocked preview on examples/secure-bucket-smoke | clean, both tiers | pnpm --filter @hulumi-examples/secure-bucket-smoke test: Test Files 1 passed (1); Tests 1 passed (1). Sandbox + Startup-Hardened bucket types captured by newResource mock, tier-appropriate deltas (ObjectLock / Logging / EventDataStore) present only on startup-hardened. |
pass | Real pulumi preview requires Pulumi CLI + network; substitute is setMocks + runtime assertions, same contract. |
| Full tests | pnpm -r test |
green | packages/baseline: 11/11. packages/policies: 20/20. examples/secure-bucket-smoke: 1/1. tests/skill-bdd (M1 regression): 19/19. Total 51/51 green. | pass | Node 24 local; CI pins Node 20 LTS via actions/setup-node. |
| E2E mock tests | component + pack + example | all green | secure-bucket.test.ts + mappings.test.ts (baseline); hulumi-hardening-pack.test.ts (policies); smoke.test.ts (example). Every BDD row has a runtime-exercised test. | pass | See docs/slo/verify/hulumi-m2.md after /slo-verify M2 runs. |
| Build / lint / typecheck | pnpm -r build && pnpm -r lint && pnpm -r typecheck |
green | baseline build: Done. policies build: Done. Typecheck Done on all 5 script-bearing workspaces. eslint Done on all 4 lint-bearing workspaces. No warnings. | pass | Added setImmediate + setInterval + clearImmediate + clearInterval to eslint.config.mjs globals to support the Pulumi mock-settle helper. |
| Exact-pin-guard | seeded drift via sed on @pulumi/pulumi integrity hash |
fails as designed | Tampered hash → exact-pin-guard: FAIL with integrity mismatch message + exit=1. Restored lockfile → exact-pin-guard: OK (3 @pulumi/* deps match pinned hashes) exit=0. |
pass | Added as scripts/exact-pin-guard.mjs, wired to .github/workflows/ci.yml before install+test jobs. Command: pnpm run lint:exact-pin-guard. |
| License-boundary lint | built dist/ + source |
no verbatim | After removing dist from SKIP_DIRS: license-boundary-lint: OK (IDs-only policy upheld across scanned trees). Scans now cover src/ AND dist/ of packages + skills. |
pass | Source-map sidecars (.map) remain excluded by extension filter. Change documented inline in the script. |
| Mappings sync | mappings.test.ts |
TS ⊆ docs | 4/4: CCM / CIS-AWS-v5.0.0 / NIST-800-53-r5 / ATLAS TS mappings are a subset of the corresponding docs/mappings/*.md tables with non-empty URLs. |
pass | Prevents source drift from the docs source-of-truth per critique E3. |
| Skill forward-ref | grep 'v0.2+' skills/hulumi-threat-model/scenarios/s3-public-bucket-hardening.json docs/threat-model-examples/s3-public-bucket-hardening.md |
zero | grep returns no matches; other scenarios still reference v0.2+ by design (those scenarios remain forward-referenced until their driving milestone ships). | pass | SecureBucket + HulumiHardeningPack availability strings updated from "v0.2+" → "v0.2" + "Shipped in M2" rationale suffix. Output schema unchanged per M1 lesson's guidance. |
| Smoke tests | Smoke Tests checklist | all checked | 8/9 manual smoke tests pass. pulumi preview --policy-pack smoke deferred to M3 (requires Pulumi CLI + sandbox); substitute is the mock-runtime smoke test which is green. |
pass | See Smoke Tests checklist above. |
| Test artifact cleanup | git status |
clean | working tree will be clean after the M2 commit; no untracked test output (dist/ gitignored, tmp files cleaned). | pass | Final commit staged before /slo-retro M2. |
| .gitignore review | new patterns present | yes | Added packages/*/dist/, examples/*/node_modules/, .pulumi/, *.pulumi.backup, Pulumi.*.yaml.bak. Top-level dist/ retained for belt-and-suspenders coverage. |
pass | pnpm-lock.yaml added to .prettierignore (machine-managed file). |
| Compatibility | M1 regression | green | tests/skill-bdd 19/19 still pass. SKILL.md unchanged. Output frontmatter schema unchanged. agentskills.io schema still validates. | pass | s3 scenario's availability strings changed (data-only edit); rendered example markdown updated to match. No frontmatter or section-structure change. |
- All M2 BDD scenarios pass.
- All E2E validations pass.
- Full M1 + M2 test suite green.
- Smoke tests checked.
- Compatibility complete.
- No forbidden shortcuts.
git statusclean..gitignoreup to date.docs/components/secure-bucket.md+docs/tiers.md+docs/components/README.mdcomplete.- M1 skill forward-reference updated; M1 regression green.
@pulumi/*exact-pinned + on CI allowlist.docs/slo/lessons/hulumi-m2.md+docs/slo/completion/hulumi-m2.mdwritten.- Milestone Tracker
done.
- ARCHITECTURE.md (Hulumi): add
@hulumi/baseline+@hulumi/policiesto Key Components; note M3 addsAccountFoundation. - README.md: quick-start snippet for
SecureBucketin both tiers. - Other docs:
docs/components/README.mdindex.
- H3 advisory-in-M2, mandatory-in-M5 is explicit phasing — mandatory before drift classifier exists creates noise without teeth.
CisV5Packstub registers only bucket-relevant CIS recommendations; full pack in M3.- CRR is not a tier delta; opt-in arg in either tier.
- Mappings sync test prevents source drift from docs source-of-truth.