Completion — Milestone 4 (Drift classifier + 4 adapters + TLA+-bound verdict matrix + 6 security BDDs)
Completed 2026-04-25.
Achieved (with the same Pulumi-Cloud-token deferral as M3 for the real-AWS integration body). @hulumi/drift ships with DriftClassifier composing four pluggable adapters (AutomationApiAdapter, CloudTrailAdapter, ProviderVersionAdapter, GitLogAdapter). Verdict logic in src/verdict.ts mirrors TLA+ HardenedVerdict from HulumiDrift.tla (upstream planning corpus); the 5-row matrix is walked verbatim by tests/verdict-matrix.feature.test.ts from a vendored copy of HulumiDrift.trace.md. The 6 security BDDs (S2 cache-perms, S3 shell-injection, S7 rate-limit, E1 probe-timeout, E4 namespace-rejection, E5 shallow-clone) all pass. The forbidden-shortcut AST lint enforces child_process-free + sleep-free packages/drift/src/ outside probes/.
124 tests pass + 7 skipped (3 baseline integration + 4 drift integration). Pipeline green: pnpm -r build, pnpm -r typecheck, pnpm -r test, pnpm -r lint, pnpm run lint:license-boundary, pnpm run lint:exact-pin-guard, pnpm run format:check.
Deferred sub-criterion: ≥1 weekly scheduled drift integration run completed against the sandbox is satisfied in CONTRACT-ONLY mode (mocks-only path). The full real-AWS drift cycle requires PULUMI_ACCESS_TOKEN configuration — same gating as M3.
packages/drift/package.json— peer-pinned@pulumi/pulumi, runtime deps@aws-sdk/client-cloudtrail,@aws-sdk/client-sts,@aws-sdk/credential-providers,simple-git,p-timeout.packages/drift/{tsconfig,tsconfig.build,vitest.config}.{json,ts}.packages/drift/src/types.ts— public types +DRIFT_SOURCESconst array; locked to TLA+Sourceset.packages/drift/src/verdict.ts—hardenedVerdict()TS mirror of TLA+ spec.packages/drift/src/monotonicity.ts—checkMonotonicity().packages/drift/src/cache.ts— on-disk cache with mode 0o600 + UID check + TTL.packages/drift/src/probe.ts—runProbe()+p-timeout+ AbortSignal (only file withsetTimeoutin src/).packages/drift/src/urn-sanitize.ts—validateUrn()+UnsafeUrnErrordefense-in-depth guard.packages/drift/src/classifier.ts—DriftClassifierorchestrator.packages/drift/src/adapters/{automation-api,cloudtrail,provider-version,git-log}.ts.packages/drift/src/index.ts— re-exports.packages/drift/README.md.packages/drift/tests/{verdict-matrix.feature,monotonicity,cache-permissions,shell-injection,shallow-clone,probe-timeout,namespace-rejection,rate-limit,tla-alignment,no-shell-exec}.test.ts.packages/drift/tests/_utils/trace-matrix.ts— vendored 5-row trace fromHulumiDrift.trace.md.packages/drift/tests/integration/drift-classify.integration.test.ts— placeholder skipped suite.
examples/drift-classify-smoke/{Pulumi.yaml,package.json,tsconfig.json,vitest.config.ts,index.ts,README.md}.examples/drift-classify-smoke/tests/smoke.test.ts— both verdicts asserted (ConsoleBreakGlass/high+ProviderApiChurn/medium).
docs/components/drift-classifier.md— per-component doc.docs/drift-classifier-deployment.md— auth, probe sentinel, cache TTL, SCP pointer, failure modes.
docs/slo/completed/RUNBOOK-hulumi.md— Milestone Tracker M4 →done.docs/components/README.md— DriftClassifier link from "arriving in M4" →drift-classifier.md.skills/hulumi-threat-model/scenarios/aws-multi-account-baseline.json+s3-public-bucket-hardening.json— DriftClassifier availabilityv0.4+→v0.4(with "Shipped in M4" suffix)..github/workflows/ci.yml— newdrift-mocksjob;examples-typecheckextended to cover drift smoke..github/workflows/weekly-integration.yml— appended drift-classify integration stage after AccountFoundation.- Root
package.json—test:drift,test:drift:integrationscripts; updatedtest:integrationto chain baseline + drift. .gitignore—.hulumi/drift-cache/.eslint.config.mjs— addedAbortController,AbortSignal,requireto TS globals.
docs/slo/completed/RUNBOOK-hulumi.md— purpose tagline, M5 Tracker row, mermaid Dogfood subgraph, Component Summary, Global Execution Rules, M5 section heading, Documentation Update Table — all de-scope cross-repo UDM binding.docs/slo/runbook-milestones/hulumi-m{1,3,4,5}.md— UDM-binding mentions reframed as sunlit-guardian's own deliverable.
- Verdict-matrix 5 rows + meta
row_count_matches_trace_md+ Row-4-never-high invariant. - Monotonicity: 5 cases (no-prior, non-decreasing, demote-refused × 2, same-confidence-allow).
- Cache permissions: 0o600 mode, UID-mismatch absence, TTL-expired absence, schema-mismatch absence.
- Shell injection:
$(...), backtick, pipe / semicolon / space — all refused; normal URN reaches git via argv. - Shallow clone:
--is-shallow-repository=true→available()=false+ remediation hint. - Probe timeout: aborts after
timeoutMs; resolves before timeout when probe is fast. - Namespace rejection:
hulumi:iac-role=trueaccepted; bareiac-role, wrong-namespace variants, non-truevalues all flow through as console events. - Rate-limit: first call invokes 4 adapters + probe; second within TTL invokes none.
- TLA+ alignment:
verdict.tscitesHulumiDrift.tla+HulumiDrift-verified.md;DRIFT_SOURCESmatches TLA+Sourceset. - No-shell-exec: zero
child_processimports /exec()/spawn(); zerosetTimeout/sleep/await new Promiseoutsidesrc/probe.ts.
examples/drift-classify-smoke/tests/smoke.test.ts— both verdicts come back as expected.
- 124 mock tests + 7 skipped (3 M3 integration + 4 M4 integration).
- New CI jobs
drift-mocksruns on every PR;examples-typecheckextended. - Weekly workflow's drift-classify stage runs after AccountFoundation in CONTRACT-ONLY mode (mocks); flips to real-AWS when
PULUMI_ACCESS_TOKENis set.
- Full M1+M2+M3 BDD suites (87 tests) still pass post-M4.
SecureBucket+AccountFoundationsnapshots unchanged.HulumiHardeningPackH1–H4 unchanged;H3_ENFORCEMENT_LEVELstilladvisory(M5 flips it).Tierunion,CisV5Packrule IDs unchanged.- AWS tag schema stable.
@pulumi/*exact pins unchanged from M3.- Skill invocation on all 5 prebuilt scenarios still produces valid output; frontmatter schema unchanged.
docs/components/drift-classifier.md(new).docs/drift-classifier-deployment.md(new).docs/components/README.md— DriftClassifier link updated.docs/slo/completed/RUNBOOK-hulumi.mdMilestone Tracker M4 →done.
.hulumi/drift-cache/(explicit; the existing.hulumi/line already covered it but the explicit pattern is documented).
git status clean after the M4 commit. Drift cache files written by tests live in OS tmpdir (mkdtempSync(tmpdir(), …)) and are removed in afterEach.
- Real-AWS drift integration test body — placeholder file
tests/integration/drift-classify.integration.test.tsasserts onlyHULUMI_INTEGRATION=1. Body (Pulumi Automation API + AWS-SDK polling + deliberate console drift fixture + teardown) lands alongsidePULUMI_ACCESS_TOKENconfiguration. M5 or post-release. - Probe sentinel deployment —
docs/drift-classifier-deployment.mddocuments the one-time sentinel-bucket setup. The Pulumi snippet uses M2'sSecureBucket; we don't ship a turnkey deployment program in M4. - Mixed verdict source emission — TLA+ allows
Mixed. CurrenthardenedVerdict()only emitsNone/ProviderApiChurn/ConsoleBreakGlass/Unknown.Mixedbecomes useful when multiple adapters report drift simultaneously; v1.1+ refinement. - CloudTrail lookup bounded retry — currently no retry. The M4 contract forbids retries exceeding probe timeout, so a small bounded retry is in-scope but deferred. Track for v1.1+.
exact-pin-guard.mjsextension — currently only checks@pulumi/*. The new drift runtime deps (@aws-sdk/*,simple-git,p-timeout) are exact-pinned inpnpm-lock.yamlbut not in the guard. M5 should decide whether to broaden the guard.
pulumi.dynamic.Resourcestill incompatible with vitest: same constraint as M3. M4 avoids dynamic.Resource entirely; the probe is a plain async function. Documented at length in M3 lessons.- Trace matrix is vendored, not parsed live:
tests/_utils/trace-matrix.tscarries a hand-mirrored copy ofHulumiDrift.trace.md. The TLA+ alignment meta-test catches theverdict.tscitation drifting; trace edits in the upstream corpus require a deliberate sync to this file. Documented in lessons. PULUMI_ACCESS_TOKENnot yet configured: weekly drift integration runs in CONTRACT-ONLY mode. Same gating as M3.- Forbidden-shortcut lint strips comments before scanning: a determined adversary could obfuscate
child_processreferences through string concatenation or computed property access. The lint catches inadvertent / direct usage; deeper static analysis is M5 follow-up. Pulumi V2 deprecation warningscarried over from M2. Cosmetic; documented under M5 interface-lock review.