Skip to content

Latest commit

 

History

History
254 lines (210 loc) · 107 KB

File metadata and controls

254 lines (210 loc) · 107 KB

Milestone 1 — Ec2PatchBaseline + Ec2PatchWaves (the wedge)

Updated 2026-05-01 per Flaw 2 (production-only tag is wrong). M1 now ships two components together: Ec2PatchBaseline (the per-wave primitive) and Ec2PatchWaves (the wave-composer). They share ~90% of implementation; landing them together in one milestone keeps the runbook at 5 milestones total. The wave model adds dev → staging → production sequencing with a CloudWatch composite-alarm health gate between waves — no Lambda.

Parent runbook: docs/slo/completed/RUNBOOK-hulumi-operations.md. Read the runbook's Carmack-Style Best Practices, the Global Execution Rules (especially Rule 0 — the Hulumi-Operations scope contract, Rule 8 — tier defaults must encode the breach risk, Rule 10MonitoringFoundation is the only SNS topic this runbook touches, and Rules 11–14 — the v4 assertions/bounds/static-analysis/debugger discipline) + the Global Entry Rules before starting.

Goal: After M1, two shippable components exist:

  • @hulumi/baseline.aws.Ec2PatchBaseline — wraps aws.ssm.PatchBaseline + aws.ssm.PatchGroup + aws.ssm.MaintenanceWindow + aws.ssm.MaintenanceWindowTarget + aws.ssm.MaintenanceWindowTask + aws.ssm.ResourceDataSync + the IAM service role into a single pulumi.ComponentResource with tier-aware defaults. Tag value enum: Patch:Group ∈ {dev, staging, production} (tightened from free-form per Flaw 2). CRC32-bucket staggering, compliance metric routed to a consumer-supplied MonitoringFoundation SNS topic ARN.
  • @hulumi/baseline.aws.Ec2PatchWaves — composes up to three Ec2PatchBaseline instances (one per wave: dev → staging → production) with sequenced MaintenanceWindow schedules and a CloudWatch composite-alarm health gate between waves. The gate is a aws.cloudwatch.CompositeAlarm whose OK state wires via Pulumi Output<bool> chain into the next wave's MaintenanceWindow.enabled field. No Lambda. No Step Functions. Sandbox tier degrades to single-wave (dev only); StartupHardened tier requires all three waves.

No new SNS topics. No Hulumi-authored Lambda. The wedge of the runbook — once M1 lands, a Hulumi consumer can pulumi up an account-level wave-based patch posture in 30 lines of TypeScript.

Context: The design record docs/slo/design/hulumi-for-operations.md commits the API shape (§ Decision: Ec2PatchBaseline shape — every line of the proposed args block in that doc is contract); the RebootOption tier-default decision (§ Decision: RebootOption default per tier — both tiers default to RebootIfNeeded); the staggering decision (§ Decision: synchronized-reboot mitigation — CRC32 mod bucketCount, fail-loud at StartupHardened). The threat model docs/slo/design/hulumi-for-operations-threat-model.md commits the seven STRIDE-derived abuse-case rows for this surface. M1 ships that exact API surface — no more, no less. The component mirrors packages/baseline/src/aws/secure-bucket.ts's pulumi.ComponentResource discipline (child registration via { parent: this }; tag triple hulumi:component/hulumi:tier/hulumi:controls emitted on resources where it makes sense; assertValidTier() reused unchanged).

Carmack-style reliability goal: Strengthen Rule 5 — make invalid states unrepresentable (the RebootOption discriminated union is the showcase of v4 in this runbook) and Rule 4 — bounded resources (the staggering.bucketCount upper limit + the complianceMetric.severities length cap are the showcase). Plus Rule 1 — debugger over guessing for the integration test: when the SSM Patch Baseline doesn't apply on the sandbox EC2, aws ssm describe-patch-baselines is the inspection command — not log-grep.

Important design rule: Ec2PatchBaseline's RebootOption default is RebootIfNeeded at BOTH tiers. The NoReboot option is available only when the consumer writes rebootOption: { kind: "NoReboot", hulumi_decision_comment: "..." } — a discriminated union that requires an explanatory comment in code. This is the load-bearing decision M1 ships. The "Sandbox tier defaulting to NoReboot to avoid surprising tenants" framing is explicitly rejected because the silent-un-patching trap (sixty-day-old kernel exploited in the wild) is a worse failure than a 04:00 UTC reboot. See hulumi-for-operations-threat-model.md § Top risks — Breach and abuse-case row tm-hulumi-ops-abuse-noreboot-without-decision.

Refactor budget: Surgical addition only. New files under packages/baseline/src/aws/ec2-patch-baseline.{ts,args.ts,outputs.ts}, packages/baseline/src/aws/ec2-patch-waves.{ts,args.ts,outputs.ts} and tests for both: packages/baseline/tests/aws/ec2-patch-baseline.test.ts, packages/baseline/tests/aws/ec2-patch-waves.test.ts plus real-AWS sandbox integration tests under packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts and ec2-patch-waves.aws-ops.test.ts. Existing files modified: packages/baseline/src/aws/index.ts (re-export both), docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker, docs/ARCHITECTURE.md (one paragraph addition), docs/components/ec2-patch-baseline.md + docs/components/ec2-patch-waves.md (one-line stubs). No changes to any existing packages/baseline/src/aws/{secure-bucket,account-foundation,monitoring-foundation,identity-alarms}.ts, no changes to packages/policies/, packages/drift/, packages/k8s-baseline/, or any existing test.

Contract Block

Field Value
Inputs Ec2PatchBaseline: new Ec2PatchBaseline(name, args) where args: Ec2PatchBaselineArgs requires: tier: Tier (Sandbox | StartupHardened); patchGroupTagValue: "dev" | "staging" | "production" (enum per Flaw 2 — refused if any other value); operatingSystem: "AMAZON_LINUX_2023" | "UBUNTU" | "WINDOWS" | "AMAZON_LINUX_2" | "REDHAT_ENTERPRISE_LINUX"; approvalRules, maintenanceWindow, complianceMetric per the prior shape. Optional staggering — required at StartupHardened.
Ec2PatchWaves: new Ec2PatchWaves(name, args) where args: Ec2PatchWavesArgs requires: tier: Tier; waves: { dev?: WaveArgs; staging?: WaveArgs; production?: WaveArgs } where WaveArgs is Ec2PatchBaselineArgs minus tier (inherited) and patchGroupTagValue (set by the wave key); complianceMetric: { snsTopicArn; severityThreshold } shared across all waves; optional waveHealthGate: { appHealthAlarmArns: pulumi.Input<string>[]; onAlarmFireDisableNextWave: boolean }. Tier-degradation: at Sandbox, only waves.dev is consumed; waves.staging / waves.production are accepted but produce no resources. At StartupHardened, all three keys must be present and non-empty.
Outputs Ec2PatchBaselineOutputs per prior shape. Ec2PatchWavesOutputs exposes: waves: { dev?: Ec2PatchBaselineOutputs; staging?: Ec2PatchBaselineOutputs; production?: Ec2PatchBaselineOutputs }, compositeAlarmArns: pulumi.Output<{ devToStaging?: string; stagingToProduction?: string }> (only present at StartupHardened with full wave set), wavesEnabled: pulumi.Output<{ dev: boolean; staging: boolean; production: boolean }> (echoes the MaintenanceWindow.enabled resolution after the gate evaluates).
Interfaces touched New stable surface: @hulumi/baseline.aws#{Ec2PatchBaseline,Ec2PatchBaselineArgs,Ec2PatchBaselineOutputs,EC2_PATCH_BASELINE_COMPONENT_TYPE,Ec2PatchWaves,Ec2PatchWavesArgs,Ec2PatchWavesOutputs,EC2_PATCH_WAVES_COMPONENT_TYPE}. Component types: "hulumi:aws:Ec2PatchBaseline", "hulumi:aws:Ec2PatchWaves". Both re-exported through the package's public API. The package's published version stays at 1.1.x; v1.2.0 ships in M5.
Data classification Internal. The milestone provisions account-level Patch Manager configuration in a sandbox AWS account during integration tests. No PII, no customer data. Patch baselines reference patch IDs and AWS-managed-baseline names — neither carries Confidential data. The complianceMetric.snsTopicArn is a reference to a topic the consumer owns; Hulumi never reads message contents.
Proactive controls in play (a) C1 Define Security Requirements — the design record IS the security-requirements record; M1 implements every "Decision" line for Ec2PatchBaseline. (b) C5 Validate All Inputs — every args field is type-narrowed; cron(...) schedule regex-validated at construction; rebootOption discriminated union refuses bare strings. (c) @hulumi/baseline.aws.SecureBucket (existing precedent) — Ec2PatchBaseline mirrors its ComponentResource discipline. (d) C9 Implement Security Logging and MonitoringcomplianceMetric routes patch-compliance failures to MonitoringFoundation SNS, the load-bearing routing decision. (e) C10 Handle All Errors and Exceptions — components throw plain Error for input violations (matches existing AWS pattern); no detail-leakage paths. (f) @hulumi/baseline.aws.MonitoringFoundation (existing precedent) — Ec2PatchBaseline's complianceMetric.snsTopicArn is wired exactly as IdentityAlarms wires to MonitoringFoundation.
Abuse acceptance scenarios Six BDD rows in the table below cite tm-hulumi-ops-abuse-N from the threat model. Slug-keyed: tm-hulumi-ops-abuse-noreboot-without-decision (NoReboot requires hulumi_decision_comment — bare { kind: "NoReboot" } refused), tm-hulumi-ops-abuse-stagger-fail-loud (StartupHardened without staggering arg refused at construction), tm-hulumi-ops-abuse-baseline-tamper (CloudTrail captures ssm:UpdatePatchBaseline — covered by existing MonitoringFoundation wiring; M1 documents that the wiring exists, no new code), tm-hulumi-ops-abuse-cw-log-delivery-alarm (M1 ships a MetricAlarm on LogDelivery.Errors for the patch-compliance metric filter), tm-hulumi-ops-abuse-no-runcmd-secrets (M1 uses only AWS-managed AWS-RunPatchBaseline SSM document — no Hulumi-authored Run Command document is allowed in the codebase), tm-hulumi-ops-abuse-runaway-window (advisory CrossGuard rule lands in M4 as O_PATCH_3; M1 does NOT enforce — but the test asserts that a window scheduled cron(*/1 * * * ? *) constructs successfully so the M4 rule has data to flag), tm-hulumi-ops-abuse-service-role-least-priv (Maintenance Window service role policy is hard-coded to the minimum: ssm:SendCommand + ssm:GetCommandInvocation + ec2:DescribeInstances only). Added 2026-05-01 per Flaw 2: tm-hulumi-ops-abuse-tag-outside-enum (patchGroupTagValue outside {dev, staging, production} refused at construction) and tm-hulumi-ops-abuse-skip-wave-gate (mid-incident manual flip of a wave's MaintenanceWindow.enabled is observable via CloudTrail routed through MonitoringFoundation.high — Hulumi documents the override path, does not block it).
Files allowed to change NEW: packages/baseline/src/aws/ec2-patch-baseline.ts, packages/baseline/src/aws/ec2-patch-baseline.args.ts, packages/baseline/src/aws/ec2-patch-baseline.outputs.ts, packages/baseline/tests/aws/ec2-patch-baseline.test.ts, packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts, docs/components/ec2-patch-baseline.md (one-line stub), docs/slo/lessons/hulumi-operations-m1.md, docs/slo/completion/hulumi-operations-m1.md. MODIFIED: packages/baseline/src/aws/index.ts (one re-export line), docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker, docs/slo/runbook-milestones/hulumi-operations-m1.md (this file — Evidence Log only, in execution), docs/ARCHITECTURE.md (one-paragraph addition under "AWS account-level baseline" section), .gitignore IF needed (Pulumi checkpoints under tests/integration/aws-ops/). Files outside this milestone's allow-list — REFUSE TO TOUCH: any packages/baseline/src/{secure-bucket,account-foundation,monitoring-foundation,identity-alarms,detective-services-enable,audit-trail}.{ts,args.ts,outputs.ts} (M2/M3 own those — adding detective-services-enable here would couple M1 to M2's open question); any packages/policies/, packages/drift/, packages/k8s-baseline/; any existing skills/; any existing examples/; any package.json at repo root.
Files to read before changing anything docs/slo/completed/RUNBOOK-hulumi-operations.md (Global Execution Rules + this milestone in full); docs/slo/design/hulumi-for-operations.md (entire doc, especially § Decision blocks); docs/slo/design/hulumi-for-operations-threat-model.md (entire doc, especially the Ec2PatchBaseline STRIDE rows); packages/baseline/src/aws/secure-bucket.ts (pattern precedent); packages/baseline/src/aws/secure-bucket.args.ts; packages/baseline/src/aws/secure-bucket.outputs.ts; packages/baseline/src/aws/monitoring-foundation.ts (the SNS-topic-output pattern Ec2PatchBaseline consumes); packages/baseline/src/aws/identity-alarms.ts (the metric-filter → alarm → SNS pattern Ec2PatchBaseline mirrors); packages/baseline/src/aws/tier.ts (the Tier enum and assertValidTier helper); packages/baseline/tests/aws/secure-bucket.test.ts (test-shape precedent); packages/baseline/tests/aws/identity-alarms.test.ts (closest precedent for testing metric-filter wiring); packages/baseline/package.json (peer-dep + dev-dep style); scripts/exact-pin-guard.mjs (verify no-op for this milestone — the runbook adds zero new @pulumi/* deps).
New files allowed All "NEW" entries in Files allowed to change.
New dependencies allowed none. Every aws.ssm.* resource used is already in @pulumi/aws (a peer dep declared by @hulumi/baseline). No @aws-sdk/* runtime imports — Ec2PatchBaseline is pure declarative IaC.
Migration allowed no — additive only. New component; no migration of existing code.
Compatibility commitments Ec2PatchBaseline, Ec2PatchBaselineArgs, Ec2PatchBaselineOutputs, EC2_PATCH_BASELINE_COMPONENT_TYPE are stable from M1 (no rename in v1.x). The RebootOption discriminated union shape ({ kind: "RebootIfNeeded" } | { kind: "NoReboot", hulumi_decision_comment: string }) is load-bearing and irreversible: changing it to a bare string would invert the breach-risk decision the design record commits to. Existing AWS / GitHub / K8s interfaces from Hulumi v1.x unchanged. The Tier enum, MonitoringFoundation.high.arn output, assertValidTier are all consumed unchanged.
Resource bounds introduced/changed (v4 Rule 4) (a) staggering.bucketCount: expected 1–10, hard limit 10, behavior at limit: refuse construction with Error("Ec2PatchBaseline.staggering.bucketCount: max 10"). Test row: bucketCount = 11 rejected. (b) approvalRules.severities length: expected 1–4, hard limit 4 (Critical, Important, Medium, Low), behavior: refuse duplicate / unknown values. Test row: ["Critical", "Critical"] rejected; ["Trivial"] rejected. (c) maintenanceWindow.durationHours × cutoffHours: AWS-side limits enforced (cutoff < duration); Hulumi adds a runtime check cutoffHours < durationHours and refuses construction otherwise. Test row: durationHours: 1, cutoffHours: 2 rejected. (d) The component creates exactly bucketCount + 5 AWS resources (1 Patch Baseline + 1 Patch Group + 1 Maintenance Window + 1 Service Role + 1 ResourceDataSync + bucketCount Maintenance Window Tasks); evidenced in test by a child-resource count assertion.
Invariants/assertions required (v4 Rule 3) Ec2PatchBaseline: (i) args.tier in ["Sandbox", "StartupHardened"] via assertValidTier(args.tier). (ii) cron(...) schedule regex-validated against six-field cron. (iii) rebootOption.kind === "NoReboot" → typeof hulumi_decision_comment === "string" && hulumi_decision_comment.length > 0. (iv) tier === "StartupHardened" → staggering !== undefined. (v) staggering.bucketCount > 0 && <= 10. (vi) complianceMetric.snsTopicArn resolves non-empty. (vii) NEW 2026-05-01: args.patchGroupTagValue ∈ {"dev","staging","production"} — refused otherwise.
Ec2PatchWaves: (viii) tier === "StartupHardened" → waves.dev !== undefined && waves.staging !== undefined && waves.production !== undefined. (ix) Wave schedules sequence correctly — dev's window cron resolves to an earlier weekday than staging's, staging earlier than production's (asserted at construction time via cron-day-extraction helper). (x) waveHealthGate.appHealthAlarmArns (when supplied) is a non-empty list of valid alarm ARNs (regex-validated). (xi) Each wave's complianceMetric.snsTopicArn echoes the parent Ec2PatchWaves.complianceMetric.snsTopicArn exactly (the wave-composer enforces shared routing — assertion fails if a wave overrides).
Debugger / inspection expectation (v4 Rule 1) The Pulumi mock-runtime test must allow inspection via pnpm --filter @hulumi/baseline test:debug -- ec2-patch-baseline if a non-obvious failure surfaces; the integration test runs against a real AWS sandbox account whose state is inspectable via aws ssm describe-patch-baselines --filters Key=NAME_PREFIX,Values=hulumi-ops-m1- --region <region>, aws ssm describe-maintenance-windows --filters Key=Name,Values=hulumi-ops-m1-* --region <region>, and aws iam get-role --role-name <serviceRoleArn-name>. If a test fails non-obviously, the executing agent runs the relevant aws describe-* and pastes output into the Evidence Log before making any speculative code change.
Static analysis gates (v4 Rule 2) pnpm -r format:check (Prettier 3.x) — must pass; pnpm -r typecheck — must pass with strict mode covering the new files; pnpm -r lint (eslint) — must pass; pnpm run lint:license-boundary — must pass (no verbatim CIS / NIST / PCI-DSS text in source); pnpm run lint:exact-pin-guard — must pass (no new @pulumi/* integrity-hash drift). No dependency-audit run needed — the dep graph is unchanged.
Forbidden shortcuts (a) NEVER ship Ec2PatchBaseline with RebootOption defaulting to { kind: "NoReboot" } at any tier — the breach-risk lever the design record commits to depends on RebootIfNeeded being the default. (b) NEVER accept a bare string "NoReboot" for rebootOption — the discriminated union with hulumi_decision_comment is load-bearing. (c) NEVER create a new SNS topic in this component (Rule 10) — complianceMetric.snsTopicArn is consumer-supplied. (d) NEVER ship a Hulumi-authored Lambda (Rule 0) — the compliance metric is a CW Logs metric filter, not a Lambda function. (e) NEVER use child_process.exec, eval, or @aws-sdk/* runtime imports in the component. (f) NEVER ship the component without staggering required at tier: StartupHardened — fail-loud is the synchronized-reboot mitigation. (g) NEVER widen the Maintenance Window service-role IAM policy beyond ssm:SendCommand + ssm:GetCommandInvocation + ec2:DescribeInstances (the abuse-case row service-role-least-priv requires the minimum). (h) NEVER swallow construction errors — every input violation throws a structured Error with the field name in the message; tests assert error messages contain field names. (i) NEVER leave a pulumi.log.warn in production paths after debug — debug pulumi.log.debug only, and remove before milestone close. (j) NEVER use a Hulumi-authored SSM Run Command document — only AWS-managed AWS-RunPatchBaseline. (k) NEVER add a placeholder for "TODO: support cross-account targets in v1.2" — open-question Q4 in the design record gates that decision; either ship cross-account in M1 (with full BDD coverage) or do NOT add the arg. M1 ships single-account only. NEW 2026-05-01 per Flaw 2: (l) NEVER accept patchGroupTagValue outside the {dev, staging, production} enum — even if a consumer asks for qa or pre-prod, refuse construction. Extending the enum is a v1.3 contract change, not an in-place addition. (m) NEVER ship Ec2PatchWaves with a Lambda or Step Function powering the gate — the gate is aws.cloudwatch.CompositeAlarm + Pulumi Output<bool> chain into MaintenanceWindow.enabled; if the implementation reaches for runtime code, the design has been departed from. (n) NEVER allow a wave to override the parent complianceMetric.snsTopicArn — shared routing is the discipline; wave-level routing override is a v1.3 ask, not M1.

Out of Scope / Must Not Do

  • No DetectiveServicesEnable — that's M2.
  • No AuditTrail — that's M3.
  • No additional patchGroupTagValue enum values beyond dev/staging/production — extending the enum is a v1.3 contract change.
  • No wave-level routing overrideEc2PatchWaves enforces shared complianceMetric.snsTopicArn across all waves.
  • No Lambda or Step Function in the wave health gate — pure CompositeAlarm + Pulumi Output<bool> chain.
  • No HulumiOperationsHardeningPack (O_* policy rules) — that's M4.
  • No /hulumi-threat-model scenario for aws-patch-compliance-lapse — that's M5.
  • No new SNS topics. EVER. (Rule 10.)
  • No Hulumi-authored Lambda. EVER. (Rule 0.)
  • No Ec2PatchBaseline.findingsRoutingSnsArn → that field name belongs to DetectiveServicesEnable; here it's complianceMetric.snsTopicArn. Don't conflate.
  • No cross-account MaintenanceWindowTarget.Targets[].TargetAccountIds — open-question Q4 in the design record gates this decision; M1 ships single-account only.
  • No examples/ec2-patch-baseline-smoke/ — that's M5 launch-readiness work.
  • No Patch Manager AppConfig / Patch Manager Application resource wrapping — focus only on Baseline + MaintenanceWindow + Target + Task + ResourceDataSync + service role.
  • No license-boundary additions in docs/mappings/ — those land in M4 alongside the policy pack.
  • No CI integration of the real-AWS sandbox test in weekly-integration.yml — that lands in M5 alongside the release. M1 ships the test file; CI gating is M5.

Pre-Flight

  1. Complete the Global Entry Rules in ../RUNBOOK-hulumi-operations.md.
  2. No docs/slo/lessons/hulumi-operations-m0.md exists. Skip "read prior lessons" with a note in the Evidence Log.
  3. Read the design record ../design/hulumi-for-operations.md end-to-end — every API decision in M1 is committed there.
  4. Read the threat model ../design/hulumi-for-operations-threat-model.md — every BDD abuse-case row in this milestone cites a tm-hulumi-ops-abuse-N row.
  5. Read files listed in Files to read before changing anything.
  6. Copy the Evidence Log template into the milestone's Evidence Log section (already present below — clone the row shape).
  7. Re-state in working notes the seven load-bearing constraints: (i) scope contract — no new SNS topics, no Lambda, no cross-account; (ii) RebootOption discriminated unionRebootIfNeeded default at both tiers, NoReboot requires hulumi_decision_comment; (iii) staggering required at StartupHardened — fail-loud; (iv) bucketCount ≤ 10 (resource bound); (v) service-role IAM policy minimum — three actions only; (vi) AWS-managed-document only — no Hulumi-authored SSM Run Command document; (vii) single-account only at M1 — cross-account is open-question Q4 deferred to v1.2.
  8. Verify the AWS CLI is available locally + the sandbox account profile is configured: aws sts get-caller-identity --profile <hulumi-sandbox-profile> should return the sandbox account ID. If not, the integration test gracefully skips with a clear message; ship the unit + mock-runtime tests anyway.

Files Allowed To Change

File Planned Change
packages/baseline/src/aws/ec2-patch-baseline.ts NEW: Ec2PatchBaseline extends pulumi.ComponentResource; mirrors secure-bucket.ts shape
packages/baseline/src/aws/ec2-patch-baseline.args.ts NEW: Ec2PatchBaselineArgs type — every field per the Contract Block; patchGroupTagValue is a string-literal union enum
packages/baseline/src/aws/ec2-patch-baseline.outputs.ts NEW: Ec2PatchBaselineOutputs type
packages/baseline/src/aws/ec2-patch-waves.ts NEW 2026-05-01: Ec2PatchWaves extends pulumi.ComponentResource; composes up to 3 Ec2PatchBaseline instances + 2 aws.cloudwatch.CompositeAlarm gates (dev→staging, staging→production)
packages/baseline/src/aws/ec2-patch-waves.args.ts NEW 2026-05-01: Ec2PatchWavesArgs type — waves: { dev?: WaveArgs; staging?: WaveArgs; production?: WaveArgs } shape; WaveArgs re-uses Ec2PatchBaselineArgs minus tier and patchGroupTagValue
packages/baseline/src/aws/ec2-patch-waves.outputs.ts NEW 2026-05-01: Ec2PatchWavesOutputs type
packages/baseline/src/aws/index.ts MODIFY: re-export Ec2PatchBaseline* + Ec2PatchWaves* symbols (8 total names)
packages/baseline/tests/aws/ec2-patch-baseline.test.ts NEW: Vitest BDD covering happy path × tier × OS, invalid input rows, abuse-case rows including tag-outside-enum
packages/baseline/tests/aws/ec2-patch-waves.test.ts NEW 2026-05-01: Vitest BDD covering: Sandbox single-wave degradation, Hardened all-three-wave requirement, composite alarm wiring, gate-fires-disables-next-wave, gate-clears-re-enables-next-wave, schedule-ordering invariant (ix), shared-SNS-routing invariant (xi)
packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts NEW: real-AWS sandbox test for Ec2PatchBaseline per prior shape
packages/baseline/tests/integration/aws-ops/ec2-patch-waves.aws-ops.test.ts NEW 2026-05-01: real-AWS sandbox test creating all three waves with staggering per wave; asserts aws cloudwatch describe-alarms --alarm-name-prefix hulumi-ops-m1- returns 2 composite alarms with the documented metric set
docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker MODIFY: M1 row → in_progress on start, done on exit
docs/slo/runbook-milestones/hulumi-operations-m1.md MODIFY (during execution only): fill Evidence Log rows
docs/slo/lessons/hulumi-operations-m1.md NEW (during exit): per the v4 lessons template
docs/slo/completion/hulumi-operations-m1.md NEW (during exit): per the v4 completion template
docs/components/ec2-patch-baseline.md NEW (one-line stub): "Patch Manager wrapper enforcing tier-aware reboot + stagger + compliance routing; full reference at M5."
docs/ARCHITECTURE.md MODIFY: append one paragraph describing the new Ec2PatchBaseline under the AWS account-level baseline section (link to design record)
.gitignore MODIFY (only if needed): add patterns for any Pulumi checkpoints under the integration-test directory

Step-by-Step

  1. Re-state milestone constraints in working notes (the seven Pre-Flight constraints). Confirm the AWS sandbox profile resolves; if not, document in Evidence Log and proceed with mock-runtime tests only.
  2. Write packages/baseline/tests/aws/ec2-patch-baseline.test.ts mock-runtime tests covering every BDD row in the table below. Run — expect failures for "module not found".
  3. Implement Ec2PatchBaselineArgs (packages/baseline/src/aws/ec2-patch-baseline.args.ts) — every field strictly typed; the RebootOption discriminated union and the staggering: StartupHardened-required constraint expressed via TypeScript discriminated unions where possible (the staggering constraint is a runtime check, not a type-level one).
  4. Implement Ec2PatchBaselineOutputs (packages/baseline/src/aws/ec2-patch-baseline.outputs.ts).
  5. Implement Ec2PatchBaseline (packages/baseline/src/aws/ec2-patch-baseline.ts) — pulumi.ComponentResource constructor that: (a) runs the six runtime invariants from the Contract Block; (b) creates the IAM service role with the three-action minimum policy; (c) creates the Patch Baseline with approvalRules mapped to aws.ssm.PatchBaseline.approvalRules; (d) creates the Patch Group; (e) creates the Maintenance Window with cron(...) schedule + duration + cutoff; (f) creates bucketCount Maintenance Window Tasks each with the documented hash-bucket selector + offset; (g) creates the ResourceDataSync writing compliance to a CW Logs group; (h) creates the metric filter on that log group; (i) creates the alarm on the metric filter routed to complianceMetric.snsTopicArn; (j) emits the tag triple on every resource where it makes sense.
  6. Re-export from packages/baseline/src/aws/index.ts.
  7. Run pnpm --filter @hulumi/baseline test -- ec2-patch-baseline — expect green for the mock-runtime BDD rows.
  8. Write packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts — gated on HULUMI_INTEGRATION=1 + HULUMI_AWS_SANDBOX_PROFILE=<name> env vars; uses prefix hulumi-ops-m1-<test-id>- on every resource name; afterAll aws ssm delete-patch-baseline + delete-maintenance-window by prefix; teardown survives partial failure. Run locally if sandbox available, otherwise document the skip.
  9. Run the full repo test suite: pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guard. All green.
  10. Complete the Self-Review Gate (top-level runbook § Self-Review Gate). Update Milestone Tracker, write lessons + completion files, update docs/ARCHITECTURE.md with the one-paragraph addition.

BDD Acceptance Scenarios

Feature: @hulumi/baseline.aws.Ec2PatchBaseline — tier-aware reboot + stagger + compliance routing

Scenario Category Given When Then Threat-model row Control
Happy path — Sandbox tier with all required args happy path mock-runtime; tier: Sandbox, patchGroupTagValue: "production", operatingSystem: "AMAZON_LINUX_2023", approvalRules with severities: ["Critical","Important"], maintenanceWindow.rebootOption: { kind: "RebootIfNeeded" }, complianceMetric wired new Ec2PatchBaseline("my-patch", args) is constructed child aws.ssm.PatchBaseline registered with approvalRules containing both severities; child Maintenance Window registered with cron schedule; one child Maintenance Window Task registered (default bucketCount: 1); IAM service role has policy with exactly ssm:SendCommand, ssm:GetCommandInvocation, ec2:DescribeInstances; outputs expose all six documented fields n/a (happy path) n/a
Happy path — StartupHardened tier with explicit staggering happy path mock-runtime; same as above but tier: StartupHardened, explicit staggering: { bucketCount: 4, bucketWindowOffsetMinutes: 15 }, explicit maintenanceWindow.schedule new Ec2PatchBaseline("my-patch", args) is constructed 4 child Maintenance Window Tasks registered (one per bucket); each Task has targetParameters filter referencing a different CRC32-mod-4 bucket; offsets are 0/15/30/45 minutes from the base cron; outputs bucketCount = 4 n/a (happy path) n/a
Happy path — RebootIfNeeded is the default at both tiers happy path mock-runtime; args.maintenanceWindow.rebootOption set explicitly to { kind: "RebootIfNeeded" } new Ec2PatchBaseline(...) is constructed the underlying MaintenanceWindowTask.taskInvocationParameters.runCommandParameters.parameters.RebootOption resolves to ["RebootIfNeeded"] (the AWS SSM document parameter shape) n/a (happy path; reinforces the design decision) n/a
Happy path — complianceMetric.snsTopicArn resolves happy path mock-runtime; complianceMetric.snsTopicArn: pulumi.output("arn:aws:sns:eu-west-2:111:topic-high") constructor child aws.cloudwatch.MetricAlarm.alarmActions contains the resolved ARN; the metric filter is on a CW Logs group whose name starts with the component instance name n/a C9 logging
Invalid input — missing tier invalid input mock-runtime; args cast through as any to bypass TS, no tier field constructor throws Error('Ec2PatchBaseline: tier must be one of [Sandbox, StartupHardened]'); no child resources registered n/a (invalid input) C5 input validation
Invalid input — bad cron(...) schedule invalid input mock-runtime; maintenanceWindow.schedule: "every Wednesday" constructor throws Error('Ec2PatchBaseline: maintenanceWindow.schedule must be a six-field cron(...) expression'); no child resources n/a C5
Invalid input — duplicate severity invalid input mock-runtime; approvalRules.severities: ["Critical", "Critical"] constructor throws Error('Ec2PatchBaseline: approvalRules.severities must not contain duplicates'); no child resources n/a C5
Invalid input — cutoffHours >= durationHours invalid input mock-runtime; maintenanceWindow.durationHours: 1, cutoffHours: 2 constructor throws Error('Ec2PatchBaseline: maintenanceWindow.cutoffHours must be less than durationHours'); no child resources n/a C5
Empty state — no EC2 instances tagged with Patch:Group value empty state real-AWS integration; no EC2 has Patch:Group=production tag pulumi up succeeds; aws ssm describe-patch-baselines returns the new baseline no patches applied at first Maintenance Window run (zero targets); compliance metric is 0 events; alarm does not fire n/a n/a
Dependency failure — complianceMetric.snsTopicArn resolves to empty string partial failure mock-runtime; complianceMetric.snsTopicArn: pulumi.output("") constructor invariant assertion (vi) fires; throws Error('Ec2PatchBaseline: complianceMetric.snsTopicArn must resolve to a non-empty string') n/a C5 + invariant (Rule 11)
Resource bound — staggering.bucketCount = 11 rejected resource bound (Rule 4) mock-runtime; tier: StartupHardened, staggering: { bucketCount: 11, bucketWindowOffsetMinutes: 5 } constructor throws Error('Ec2PatchBaseline.staggering.bucketCount: max 10'); no child resources n/a Rule 4 + C5
Invariant — tier: StartupHardened without staggering rejected assertion violation mock-runtime; tier: StartupHardened, staggering: undefined constructor throws Error('Ec2PatchBaseline: staggering is required at tier StartupHardened (synchronized-reboot mitigation)'); no child resources tm-hulumi-ops-abuse-stagger-fail-loud Rule 11 + C5
Compatibility — existing MonitoringFoundation outputs still consumed unchanged compatibility mock-runtime; component receives a MonitoringFoundation.high.arn output as complianceMetric.snsTopicArn constructor the ARN is wired into MetricAlarm.alarmActions exactly once; no copy of the topic policy is added; no new SNS resource is registered n/a Rule 10
Abuse case — NoReboot without hulumi_decision_comment rejected abuse case mock-runtime; args cast through as any to write rebootOption: { kind: "NoReboot" } (no comment) constructor throws Error('Ec2PatchBaseline.maintenanceWindow.rebootOption: kind=\"NoReboot\" requires hulumi_decision_comment to be a non-empty string'); no child resources tm-hulumi-ops-abuse-noreboot-without-decision Forbidden shortcut (b) + Rule 11
Abuse case — service role IAM policy is exactly the three-action minimum abuse case mock-runtime; happy-path args constructor child aws.iam.RolePolicy.policy JSON document parses to exactly one Statement with Action: ["ssm:SendCommand", "ssm:GetCommandInvocation", "ec2:DescribeInstances"] (sorted) and Resource: "*"; assertion fails the test if any other action is present tm-hulumi-ops-abuse-service-role-least-priv Forbidden shortcut (g) + Rule 11
Abuse case — uses AWS-managed AWS-RunPatchBaseline document only abuse case mock-runtime; happy-path args constructor every MaintenanceWindowTask.taskArn resolves to literal "AWS-RunPatchBaseline" (the AWS-managed SSM document name); no Hulumi-authored document is referenced tm-hulumi-ops-abuse-no-runcmd-secrets Forbidden shortcut (j)
Abuse case — CW Logs delivery-failure alarm exists abuse case mock-runtime; happy-path args constructor a child aws.cloudwatch.MetricAlarm is registered targeting the LogDelivery.Errors metric on the compliance log group, alarm action routed to complianceMetric.snsTopicArn tm-hulumi-ops-abuse-cw-log-delivery-alarm Rule 11
Schema / compatibility — Ec2PatchBaselineArgs shape lock schema / compatibility packages/baseline/src/aws/ec2-patch-baseline.args.ts exists tests/skill-bdd/operations-args.test.ts (added in this milestone if absent) runs every documented field present with documented types; tier, patchGroupTagValue, operatingSystem, approvalRules, maintenanceWindow, complianceMetric are required; staggering is optional; the RebootOption discriminated union is exposed as a public type n/a type-layer schema lock

BDD addendum — Ec2PatchWaves (added 2026-05-01 per Flaw 2)

Feature: @hulumi/baseline.aws.Ec2PatchWaves — wave-sequenced patching with composite-alarm health gate

Scenario Category Given When Then Threat-model row Control
Happy path — Sandbox single-wave degradation happy path tier: Sandbox, waves: { dev: { ... } } only constructor one Ec2PatchBaseline registered (the dev wave); zero aws.cloudwatch.CompositeAlarm; outputs wavesEnabled: { dev: true, staging: false, production: false } n/a tier-degradation discipline
Happy path — StartupHardened all-three-wave happy path tier: StartupHardened, all three waves keys with valid WaveArgs constructor three Ec2PatchBaseline children registered; two aws.cloudwatch.CompositeAlarm registered (devToStaging, stagingToProduction); each composite alarm aggregates prior wave's SSM-Compliance-Failed metric + consumer's appHealthAlarmArns n/a n/a
Happy path — gate fires → next wave's MaintenanceWindow disabled happy path tier: StartupHardened; mock-runtime: devToStaging composite alarm in ALARM state re-run pulumi up with mock alarm state the staging wave's MaintenanceWindow.enabled resolves to false via Output<bool> chain; outputs wavesEnabled.staging: false n/a rollback-as-IaC (Rule 11)
Happy path — gate clears → next wave re-enabled on next pulumi up happy path gate previously fired, then alarm returns to OK next pulumi up staging wave's MaintenanceWindow.enabled resolves to true n/a flop-not-latch semantics
Invalid input — StartupHardened missing one of the three waves invalid input tier: StartupHardened, waves: { dev: { ... }, production: { ... } } (no staging) constructor throws Error('Ec2PatchWaves: tier StartupHardened requires all three waves (dev, staging, production); missing: staging') n/a invariant (viii)
Invalid input — wave overrides complianceMetric.snsTopicArn invalid input one wave's WaveArgs includes a complianceMetric key (which WaveArgs should refuse at the type level) constructor TypeScript rejects at compile time (preferred); at runtime via as any, throws Error('Ec2PatchWaves: per-wave complianceMetric override not supported in v1.2; shared routing only') n/a invariant (xi)
Abuse case — patchGroupTagValue outside enum refused abuse case mock-runtime: Ec2PatchBaseline constructed with patchGroupTagValue: "experiment" (cast through as any) constructor throws Error('Ec2PatchBaseline: patchGroupTagValue must be one of [dev, staging, production]; got: "experiment"') tm-hulumi-ops-abuse-tag-outside-enum invariant (vii) + Forbidden (l)
Abuse case — manual flip of MaintenanceWindow.enabled is observable abuse case real-AWS integration: a human operator runs aws ssm update-maintenance-window --window-id <id> --enabled mid-incident (operator action, not Hulumi code path) CloudTrail captures the ssm:UpdateMaintenanceWindow event; existing MonitoringFoundation wiring (M5/#46) routes it through MonitoringFoundation.high SNS; assertion in integration test checks the CloudTrail event lands in CW Logs tm-hulumi-ops-abuse-skip-wave-gate observability discipline
Resource bound — 4 waves rejected resource bound waves includes a 4th key (e.g., qa) — TS should reject; via as any constructor throws Error('Ec2PatchWaves: only dev / staging / production waves supported in v1.2') n/a Rule 4
Compatibility — Ec2PatchBaseline standalone usage unchanged compatibility consumer instantiates a single Ec2PatchBaseline directly (no Ec2PatchWaves) constructor works exactly as the original M1 contract block describes; tag-outside-enum is the only added refusal vs. v0 of M1 n/a additive surface

Regression Tests

  • All AWS BDD scenarios in packages/baseline/tests/aws/ continue to pass (no rename, no behavioral change to SecureBucket, AccountFoundation, MonitoringFoundation, IdentityAlarms).
  • All GitHub BDD scenarios in packages/baseline/tests/github/ continue to pass.
  • All K8s BDD scenarios in packages/k8s-baseline/tests/ continue to pass.
  • pnpm run lint:license-boundary continues to pass on the existing surface.
  • pnpm run lint:exact-pin-guard passes; no new @pulumi/* deps introduced.
  • Skill SKILL.md continues to validate against the agentskills.io schema (no skill changes in M1).
  • packages/drift/tests/tla-alignment.test.ts continues to pass (this milestone does not touch verdict.ts or HulumiDrift.tla).

Compatibility Checklist

  • Ec2PatchBaseline, Ec2PatchBaselineArgs, Ec2PatchBaselineOutputs, EC2_PATCH_BASELINE_COMPONENT_TYPE documented in docs/components/ec2-patch-baseline.md (one-line stub adequate; full reference doc in M5).
  • No new dependencies introduced — pnpm-lock.yaml diff has no @pulumi/* additions or version bumps.
  • pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guard green on Node 20 LTS.
  • License header present on every new .ts source file.
  • DCO sign-off required on every commit (CI enforcement carries over).
  • All existing AWS + GitHub + K8s BDD scenarios produce valid output unchanged.
  • No child_process.exec, eval, @aws-sdk/* runtime imports, or shell-interpolation in any new file — tests/no-shell-exec.test.ts (existing) covers packages/*/src/; verify glob includes the new file.
  • Tier enum unchanged; MonitoringFoundation outputs unchanged; assertValidTier unchanged.

E2E Runtime Validation

File: packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts.

E2E Test What It Proves Pass Criteria
creates_patch_baseline_against_real_sandbox Ec2PatchBaseline works end-to-end against real AWS Test creates a Pulumi stack with Ec2PatchBaseline("hulumi-ops-m1-<test-id>", { tier: Sandbox, ... }); aws ssm describe-patch-baselines --filters Key=NAME_PREFIX,Values=hulumi-ops-m1-<test-id>- --region <region> returns exactly one entry whose OperatingSystem matches input; teardown deletes the baseline and Maintenance Window cleanly.
staggering_bucketCount_4_creates_4_window_tasks The CRC32-bucket-stagger decision is honored on real AWS Test creates tier: StartupHardened with staggering.bucketCount: 4; aws ssm describe-maintenance-window-tasks --window-id <id> returns exactly 4 tasks; each task's Targets[0].Values[0] filter is distinct and references one of the four hash-bucket values; offsets are 0/15/30/45 minutes apart in Schedule parameter.
service_role_policy_is_three_action_minimum The least-privilege abuse case holds end-to-end After pulumi up, aws iam list-attached-role-policies --role-name <serviceRoleName> returns one inline policy whose Action is exactly ["ssm:SendCommand", "ssm:GetCommandInvocation", "ec2:DescribeInstances"]; assertion fails if any additional action present.
cw_log_delivery_alarm_exists_with_correct_target The CW Logs delivery-failure alarm wiring is correct After pulumi up, aws cloudwatch describe-alarms --alarm-name-prefix hulumi-ops-m1-<test-id>- returns at least one alarm with MetricName: LogDelivery.Errors, AlarmActions containing the complianceMetric.snsTopicArn that was passed in.

The integration test gracefully skips with a clear message when HULUMI_INTEGRATION is not set or when aws sts get-caller-identity --profile <hulumi-sandbox-profile> fails. The test runs locally during execution and lands in the weekly-integration matrix in M5.

Smoke Tests

  • pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guard → all green.
  • (Optional, requires AWS sandbox) HULUMI_INTEGRATION=1 HULUMI_AWS_SANDBOX_PROFILE=<name> pnpm --filter @hulumi/baseline test:integration:aws-ops → integration test green; resources deleted after run.
  • In a Pulumi program importing @hulumi/baseline: new Ec2PatchBaseline("foo", { tier: "StartupHardened", ... no staggering ... } as any) causes pulumi preview to fail with the documented missing-staggering error.
  • In a Pulumi program: new Ec2PatchBaseline("foo", { ..., maintenanceWindow: { ..., rebootOption: { kind: "NoReboot" } } } as any) causes pulumi preview to fail with the documented missing-comment error.
  • git status shows no untracked test artifacts.
  • .gitignore covers any new generated files (Pulumi checkpoints under tests/integration/aws-ops/ are gitignored if they appear).

Evidence Log

Step Command / Check Expected Result Actual Result Pass/Fail Notes
Baseline tests pnpm -r build && pnpm -r test green pre-M1 filled during execution pending Captures pre-M1 baseline so any regression is attributable to M1
BDD tests created pnpm --filter @hulumi/baseline test -- ec2-patch-baseline (pre-impl) fail with module-not-found / wrong-shape errors filled during execution pending Pre-implementation failure shape captured
E2E stubs created cat packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts file present, it.skip(...) rows for every E2E table row filled during execution pending Stubs land before implementation
Implementation filesystem source files present and re-exported from index.ts filled during execution pending After implementation
Formatter pnpm -r format:check clean filled during execution pending
Typecheck pnpm -r typecheck clean filled during execution pending
Static analyzer pnpm -r lint clean (no new warnings) filled during execution pending
License-boundary lint pnpm run lint:license-boundary OK filled during execution pending Existing lint scope unchanged in M1
Exact-pin guard pnpm run lint:exact-pin-guard OK with no new @pulumi/* deps filled during execution pending No-op for this milestone
Dependency audit (if deps changed) pnpm audit --audit-level=high n/a — dep graph unchanged filled during execution pending Skip with note
Mock-runtime BDD pnpm --filter @hulumi/baseline test -- ec2-patch-baseline all BDD rows pass filled during execution pending Every row from the BDD table covered
Full tests pnpm -r test green filled during execution pending
Real-AWS integration test HULUMI_INTEGRATION=1 HULUMI_AWS_SANDBOX_PROFILE=<name> pnpm --filter @hulumi/baseline test:integration:aws-ops green or skipped filled during execution pending Skip cleanly if AWS sandbox unavailable
Build/boot pnpm -r build builds cleanly; dist/ present filled during execution pending
Resource-bound verification (Rule 4) pnpm --filter @hulumi/baseline test -- ec2-patch-baseline -t "bucketCount = 11" bound encoded; near-limit tested filled during execution pending Asserts bucketCount=10 succeeds, =11 rejected
Invariant/assertion verification (Rule 3) pnpm --filter @hulumi/baseline test -- ec2-patch-baseline -t "staggering required at StartupHardened" assertion fires; structured error returned filled during execution pending Covers the StartupHardened-without-staggering invariant
Debugger / state inspection (Rule 1) aws ssm describe-patch-baselines --filters Key=NAME_PREFIX,Values=hulumi-ops-m1- --region <region> hypothesis confirmed before any speculative code change filled during execution pending Used only if integration test fails non-obviously
Smoke tests (manual list above) all checked filled during execution pending
Test artifact cleanup git status --short only intentional new files + modified tracker filled during execution pending No transient test artifacts
.gitignore review existing .gitignore covers Node/pnpm/Vitest/TLA+/Pulumi-checkpoint no change needed unless aws-ops integration test drops state in repo filled during execution pending
Compatibility checks (manual list above) no regressions filled during execution pending

Definition of Done

The milestone is done only when all of the following are objectively true:

  • All BDD scenarios pass (mock-runtime always; real-AWS integration when sandbox available).
  • All E2E runtime validation tests pass (or skip cleanly with documented reason).
  • pnpm -r format:check, pnpm -r typecheck, pnpm -r lint, pnpm run lint:license-boundary, pnpm run lint:exact-pin-guard all green.
  • Smoke tests checked off.
  • Compatibility checklist complete.
  • Resource bounds (v4 Rule 4) encoded and tested (bucketCount ≤ 10 + severities length 1–4 + cutoffHours < durationHours).
  • Invariants/assertions (v4 Rule 3) encoded and tested (the six invariants in the Contract Block).
  • No forbidden shortcuts present.
  • git status clean.
  • .gitignore covers all new generated files.
  • All existing AWS + GitHub + K8s BDD scenarios produce valid output unchanged (regression-tested).
  • Self-Review Gate (top-level runbook) answered with yes on every question or documented exception.
  • docs/slo/lessons/hulumi-operations-m1.md written per the v4 lessons template (incl. assumptions verified / unresolved, invariants added, resource bounds, debugging notes).
  • docs/slo/completion/hulumi-operations-m1.md written per the v4 completion template.
  • Milestone Tracker in docs/slo/completed/RUNBOOK-hulumi-operations.md updated to done.

Post-Flight

  • docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker → M1 done.
  • docs/slo/completed/RUNBOOK-hulumi-operations.md Component Summary Table — verify M1 row matches what was actually shipped.
  • docs/components/ec2-patch-baseline.md — one-line stub if not present (full reference doc in M5).
  • docs/ARCHITECTURE.md — one-paragraph description of Ec2PatchBaseline under the AWS account-level baseline section (link to design record).
  • docs/issue-candidates.md — no strikes in M1 (M1 doesn't close any open issue; #47 closes in M3, #49 closes in M2).

Notes

  • This milestone ships no DetectiveServicesEnable, no AuditTrail, no O_* policy rules, no new threat-model scenario — those are M2 / M3 / M4 / M5.
  • Prior-lessons coverage category does not apply (greenfield for the Operations variant).
  • The RebootOption discriminated union ({ kind: "RebootIfNeeded" } | { kind: "NoReboot", hulumi_decision_comment: string }) is the single most important design decision in this milestone — it must be recorded in the lessons file as a deliberate inversion of the "Sandbox = NoReboot" trap and as the load-bearing reason Ec2PatchBaseline ships at all. The threat-model row tm-hulumi-ops-abuse-noreboot-without-decision is the specific abuse-case row this design decision answers.
  • The real-AWS integration test is gated on HULUMI_INTEGRATION=1 + a configured sandbox profile. CI in this milestone runs only mock-runtime BDD; the aws-ops suite is exercised in M5's release readiness smoke + the weekly-integration workflow extension.
  • Open-question Q4 from the design record (MaintenanceWindowTarget.Targets[].TargetAccountIds — cross-account targets) is explicitly deferred to v1.2; M1 ships single-account only. Document the deferral in the lessons file with a pointer to the open question.