feat(compute): Lambda MicroVMs P2 — smoke parity foundations (#645) - #733
feat(compute): Lambda MicroVMs P2 — smoke parity foundations (#645)#733dreamorosi wants to merge 8 commits into
Conversation
…ples#645) Implement Phase P2 of ADR-021 short of the live smoke run: the agent is now fully launchable and observable on the lambda-microvm backend. Agent (agent/src/server.py): - platform_config delivery: deployment-specific, non-secret identifiers (13-key allowlist, 4 required) arrive in the /run envelope and are installed into the environment before any credential or pipeline initialization; unknown keys fail closed (400 MICROVM_RUN_PLATFORM_CONFIG_INVALID / _INCOMPLETE). Decided over image-baked env by configuration lifetime: platform values belong to the deployment, image versions to packaging - and external images cannot receive stack env at all - /validate (image hook): shallow, zero AWS calls - it runs under the build role; /terminate (runtime hook): best-effort flush, returns 200 for any body (raw-Request handler, structural guard against reintroducing a typed body model), never writes terminal task status - snapshot credential hygiene: /ready no longer spins up the CloudWatch writer (it pinned a build-time region + resolved credential chain into boto3.DEFAULT_SESSION - state a snapshot would replay into every MicroVM); /run pre-install logging is stdout-only until platform_config is installed (poisoned-seam + subprocess regression tests, mutation-checked) - the /run S3 payload fetch now uses the attributed client factory Infra (cdk): - platform_config producer in the strategy: closed map over the contract keys, env read at call time, required-key guard with remedy, boundary math includes the block, size-check before PutObject, key-names-only logging - execution-role runtime IAM parity (feature-based, not an ECS copy): GitHub PAT + channel-OAuth-prefix secrets, scoped Bedrock invocation, AgentCore Memory grantReadWrite, AZ describe; still no direct DynamoDB (tenant data flows through the SessionRole) - heartbeat liveness extended to lambda-microvm (exhaustive per-backend switch; agentcore byte-identical, ecs excluded; RUNNING-scoped so P3 suspends stay immune) - closes the hung-in-guest-pipeline blind spot behind a healthy substrate - hooks declared to match what the agent serves: ready+validate (image, 60s) and run+terminate (runtime, 60s/15s), with both-direction exact-set tests; packaging script hooks JSON updated Contracts: microvm_platform_config in contracts/constants.json is the single source of truth; both the agent and the CDK strategy derive from it and check-constants-sync validates shape and forbids literal re-declarations. Docs: ADR-021 amended in place (canonical wire shapes, per-phase hook table, platform-config delivery, dual-signal liveness); COMPUTE.md gains the Lambda MicroVMs column + classic-Lambda distinction (aws-samples#645 acceptance criterion) and backend overview; ORCHESTRATOR.md gains the dual-signal liveness and lifecycle sections; SECURITY.md covers all three compute roles. Remaining for P2 completion: the live smoke run (clone -> change -> PR with bgagent watch) and the deferred empirical items (suspend TTL >1h, SUSPENDED-vs-quota, microvmImageHooks API spelling, NO_INGRESS ARN) - tracked on aws-samples#645. Verification: cdk 3825, agent 1590 (coverage 82.8%), cli untouched; tsc/eslint/ruff/ty/vulture clean; drift-prevention (constants-sync, types-sync, pins) green; docs sync idempotent; docs:check clean. security:sast runs in CI (semgrep unavailable locally). Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
'sk-ant-secret' pattern-matches Anthropic key detectors (Code Defender warned; CI gitleaks could fail). Replaced with a non-matching dummy; test intent unchanged (secret values must not leak into session-start logs). Full P2 diff swept for other detector-pattern lookalikes: none. gitleaks local scan clean. Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
…#645) The Stage D live smoke run (evidence: aws-samples#645 thread) failed at turn 0 and surfaced five live-contract defects invisible to synth, unit tests, and cdk-nag. This lands all corrections: - P2-F1/F3: drop aws:SourceAccount from all MicroVM-facing role trust policies - the service presents no source key when assuming them (deterministic connector CREATE_FAILED; misleading caller- side PassRole denial proven by elimination). ADR security table states the limitation honestly with per-role passer accounting (CloudFormation passes build/operator roles; only the orchestrator passes the execution role) and the qualified Resource:* exceptions - P2-F5: cold 225 MiB claude binary killed every task (10s version probe vs lazy snapshot hydration). /ready now warms claude (required, 120s) then git/node (optional, shared 240s ceiling inside the 300s hook budget; hung optional can never starve the snapshot); runner probe raised to 60s. Fake-clock budget tests - P2-F2: CFN enforces API enums at change-set time, refuting the string-shape reasoning - ARM_64 + ENABLED hook states; routes live only in MICROVM_AGENT_HOOK_ROUTES; negative test keeps route strings out of the image resource; new drift-guard test diffs the script's actual flags against the synthesized template - P2-F4: execution role granted CreateLogStream/PutLogEvents on the application log group whose name travels in platform_config - P2-F6: ADR corrected - the service reaps run-hook FAILURES (4xx -> ~12s terminate); active terminate retained for success paths (after a 200 the service has no view of the guest) - P2-F8: empty terminate-hook microvmId is expected-normal; artifacts/trace bucket sameness documented as intentional Fixed-but-not-re-exercised: the CDK-managed image path and the warm-up's effect on snapshot warmth are proven against the run's verbatim errors; the follow-up live re-run converts them. cdk 3832, agent 1609, cli 736; build/drift-prevention/docs green. Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
Clears GHSA-5p4m-2wfm-xmqj (High) flagged by osv-scanner. The root resolutions range ^4.2.0 already admits 4.3.1; minimal single-entry lock refresh under the CI toolchain (Node 22.23.2 + Yarn 1.22.22), byte-identical across repeated installs. osv clean; cdk 3832 and cli 736 green. Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #733 +/- ##
=======================================
Coverage ? 91.87%
=======================================
Files ? 294
Lines ? 82848
Branches ? 9037
=======================================
Hits ? 76120
Misses ? 6728
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…-samples#645) The Stage D-redux smoke run achieved the P2 acceptance criterion (clone -> change -> PR on the lambda-microvm backend: dreamorosi/batch-sync-triage#6, 153s, $0.28, 12 turns) and converted four of five Stage E fixes live. It also disproved run 1's exoneration of the identity-side PassRole condition via a controlled two-arm experiment - run 1's control was contaminated by its own temporary unconditioned grant. This lands the residual fixes: - MicrovmPassExecutionRole: drop iam:PassedToService (denied on the RunMicrovm path; two-arm evidence in the reversed comment); the exact execution-role ARN remains the scoping. Tests assert no Condition, exact ARN, no wildcard - Bootstrap 1.4.0: new MicrovmPassRoles statement in the backend-conditional compute-lambda-microvm policy so CloudFormation can pass the build/connector-operator roles for the CDK-managed image path (three-leg evidence: byte-identical live policy, simulate allowed-with/implicitDeny-without, same-role out-of-band control). Execution role deliberately excluded; infrastructure's allowlisted IAMPassRole untouched and now test-pinned. Golden DEPLOYMENT_ROLES block + re-bootstrap callout; operators must re-bootstrap to >= 1.4.0 - agent_heartbeat_at now projected through toTaskDetail and shown in bgagent status/detail renderers (its absence caused run 1's wrong liveness conclusion while DynamoDB held a 6s-old value) - ADR: per-role passer accounting (orchestrator passes the execution role in practice; the deploy role's prefix grant technically matches it), smoke status corrected to record the passing run and the two fixes still awaiting live re-exercise, template-size consequence at 98.6% (aws-samples#735) Honest residuals: P2r2-F9/F10 fixes are evidence-based but not yet re-exercised live; suspend TTL beyond 1h and SUSPENDED-vs-quota remain open (AWS-side observability gaps). cdk 3841, cli 745, agent 1609; build/drift-prevention/bootstrap determinism/docs all green. Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
Two branches: the exhaustive unknown-compute-type guard in the heartbeat liveness check (rejects rather than bypassing), and run_agent's wiring of the extracted claude version probe. Patch coverage 100% on both files. cdk 3842, agent 1610. Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
…mples#645) Pre-review of PR aws-samples#733 per .abca/commands/review_pr.md surfaced six blockers and a set of nits; all addressed: - B2: the no-platform_config /run branch logged via _warn_cw before anything was installed, violating the PR's own pre-install stdout-only EARS rule (the P2 build-hook defect one phase later); routed through _pre_config_log - B3: the seam-guard test could not catch B2 - it disarmed on any _install_platform_config return including the vacuous no-config early-return; now disarms only on non-empty installs, with _extract_invocation_params as the documented legacy-path phase marker, and the no-config case joined the armed parametrization - B4: the diagnostics-only claude version probe could still kill a task (TimeoutExpired/OSError propagated); now warns and continues, with parametrized non-fatality tests - B5: the /ready budget invariant lived as a hardcoded 300 in a Python test; both budgets now derive from contracts/constants.json (microvm_hook_budgets) with ordering invariants and no-literal redeclaration checks in check-constants-sync - B6: SECURITY.md now states the MicroVM compute-role delta explicitly (no confused-deputy trust condition - service limitation, evidenced) with the complete grant enumeration - B1: ADR-021 section 4 is evidence-self-sufficient (verbatim CREATE_FAILED, simulate-principal-policy both arms, out-of-band control, contaminated-control chronology); the P1+P2 verification runbooks are now COMMITTED under docs/verification/ with IAM unique-IDs, account IDs, emails, and VM-specific endpoints redacted (gitleaks + Code Defender clean) - Nits: iam:PassRole recorded in resource-action-map for the two MicroVM CFN types; bedrock-models JSDoc reattached; /terminate active count uses None-for-unknown; /validate 503 documented as a refactor tripwire; trust-comment block trimmed to conclusion + ADR pointer (evidence lives once); frozen warning-id note; whitespace; two cdk/AGENTS.md Common-mistakes bullets (L1 string enums validate only at change-set time; statement-level bootstrap additions still need re-bootstrap + MINOR bump) cdk 3843, cli 745, agent 1622 (83.17% cov); build, drift-prevention, bootstrap determinism, docs sync all green. Refs aws-samples#645 Co-authored-by: Claude <noreply@anthropic.com>
… into feat/645-lambda-microvm-p2 Upstream gained one commit — c927a20 "feat(orchestration): admission queue with deferred pickup (aws-samples#441) (aws-samples#544)" — which overlaps this branch in the same 13 files the heartbeat/MicroVM work touches. Both change sets are kept; only three files needed manual resolution. Auto-merged, verified by hand (disjoint regions, no re-seating needed): - shared/orchestrator.ts — aws-samples#544 adds `queueTask()` between `finalizeTask` and `failTask`; it does NOT touch `PollState` or `pollTaskStatus`, so `heartbeatLivenessApplies`, `buildComputeMetadata` and `reconcileMicrovmSubstrateState` stay exactly where they were seated. - orchestrate-task.ts — aws-samples#544 rewrites the `admission-control` step (queue instead of fail) and the Linear/Jira cap-feedback copy; ours owns `start-session`, the MicroVM poll cross-check and `finalize`. Import list unions `queueTask` with `buildComputeMetadata` / `reconcileMicrovmSubstrateState`. - shared/types.ts + cli/src/types.ts — `TaskDetail` carries BOTH additions in the same order on both sides (`agent_heartbeat_at` in the timestamp block, `queued_at` / `queue_position` / `estimated_wait_s` appended after `awaiting_approval_request_id`), so `check:types-sync` still matches exactly. `toTaskDetail` keeps its new `queueInfo` parameter and maps the heartbeat. - cli/src/format.ts — both renderers show both fields: `Queue: position N` with the pre-start config lines (after `Branch`), `Heartbeat:` with the temporal block (after `Completed` / next to `Last event:`). The two are mutually exclusive in practice — a QUEUED task has no heartbeat, and a beating task has no queue position. - cancel-task.ts — the new `QUEUED` cancel path cannot reach the MicroVM `TerminateMicrovm` branch: that block is gated on `wasRunning && runtimeSessionId`, neither of which a queued task has. Resolved manually: - docs/design/ORCHESTRATOR.md (+ Starlight mirror) — transition table takes aws-samples#544's four `QUEUED` rows plus its reworded `SUBMITTED -> FAILED`, and keeps our backend-agnostic `HYDRATING -> RUNNING` wording; admission-control step takes aws-samples#544's queue semantics for the concurrency cap and rate limiting, and keeps our "configured system limit and selected-backend quotas" for system concurrency. `mise //docs:sync` reproduces the mirror byte-for-byte. - yarn.lock — the js-yaml descriptor follows the root `resolutions` bump ^4.2.0 -> ^4.3.1 that came with aws-samples#544; the pinned 4.3.1 version/integrity was already identical on both sides, and the MicroVM SDK entries merged clean. `yarn install --check-files` reports the lock already up to date. Verification: `mise run build` (agent 1622, cdk 183 suites / 3885, cli 56 suites / 750, docs build + link check, types/constants/coverage/transitive-pin drift checks) green; `mise run drift-prevention` green; `mise //docs:sync` and `mise //cdk:bootstrap:generate` both no-ops; `mise //cdk:eslint` and `mise //cli:eslint` (--fix) produce no mutation.
|
Pre-review pass (self-commissioned, per Pre-review found 6 blockers; all fixed in The verification runbooks are now committed ( One PR-body correction (pre-review caught it): the body said
Also filed #736 so the "re-tighten IAM when AWS exposes usable condition keys" revisit has a handle. @krokoko this is the fourth main-merge this PR has absorbed (#695, #711-#718, #345+#704, #544) — grateful for a prompt look when you have one, or auto-merge-on-approval if that's easier. |
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Strong architecture, security posture, and test discipline — CI is green (4/4), the bootstrap bundle verified clean by execution, and platform_config is a genuine security improvement over image-frozen env. Gated on seven items, all small and local. Four of them sit on the diagnosis path, and one hits the failure mode this backend hits most often.
The unifying theme: this PR is excellent at failing closed, and weaker at explaining why it failed. Rejections are correct, distinctly coded, and tested — then the code either discards the reason, routes it to a classifier that renames it, or documents a mechanism that isn't real.
Blocking
B1. stateReason is discarded, so the most common failure gets a fabricated cause
cdk/src/handlers/shared/strategies/lambda-microvm-strategy.ts:691-744
pollSession reads result.state and ignores stateReason. Your own runbook (docs/verification/645-p2-smoke-runbook.md:771) records what that field says on the dominant runtime failure:
state = TERMINATED
stateReason = Run lifecycle hook returned HTTP status 400. Please check your hook endpoint...
TERMINATED → {status:'completed'} has no error slot, so orchestrator.ts:451 builds detail = "substrate state completed" and the operator gets:
MicroVM substrate terminated before the agent wrote a terminal status: substrate state completed
…with a remedy naming "session duration cap, host fault, or an external terminate" — none of which happened. Every distinct wire code /run carefully emits (MICROVM_RUN_PLATFORM_CONFIG_INVALID, TASK_RECORD_INCOMPLETE) is thrown away one layer up. Per this repo's own standard, a plausible-but-wrong result is a defect.
Fix: thread stateReason into the reconcile detail (minimum: logger.warn it plus append to the message).
B2. The new required-key throw is classified TRANSIENT and auto-retried
cdk/src/handlers/shared/strategies/lambda-microvm-strategy.ts:282-291
I ran the real classifier regexes. This message carries no MICROVM_ERROR_MARKER, so it falls to the /Session start failed/i catch-all (error-classifier.ts:320) → retryable: true, remedy "Check AgentCore Runtime or ECS cluster health." A hand-edited-Lambda-environment fault, which retrying provably cannot fix, gets retried and misattributed to the wrong substrate. This is exactly what the marker section's own comment calls "mandatory, not decorative." Tests assert the message text thoroughly but never its classification.
Fix: wrapMicrovmError('platform config', …) or a classifier entry — plus a test asserting classification, not just message.
B3. orchestrator.ts:82-85 documents an invariant the code does not have — in the direction that breaks ECS
"The agent writes that timestamp UNCONDITIONALLY on every substrate… so this predicate decides only whether the ORCHESTRATOR acts on it."
Verified false. pipeline.py:919 writes once; the only periodic writer is _heartbeat_worker (server.py:272, 45 s), started solely from _run_task_background — reachable only via /invocations or the MicroVM /run hook. ECS bypasses uvicorn entirely (ecs-strategy.ts:245: "This bypasses the uvicorn server entirely").
So ecs => false is a hard correctness constraint, not the tuning preference the comment frames it as. Anyone consolidating those "two independently-tuned kill paths" would flip it to true and fail every ECS task after ~6 minutes (grace 120 s + stale 240 s). The comment invites the change that breaks the backend.
Fix: lead the ecs bullet with the hard constraint — "ECS never starts _heartbeat_worker; enabling this fails every ECS task after ~6 min."
B4. The boto3.DEFAULT_SESSION region-pinning claim is factually wrong
agent/src/server.py:1084-1088, 1105-1107, 1122-1123
Tested in this repo's own venv (botocore 1.43.42), controlled for ambient AWS_PROFILE/~/.aws/config:
BUILD session.region_name: us-west-2 | logs client: us-west-2
AFTER session.region_name: eu-west-1 | logs client: eu-west-1
creds frozen to build role: AKIAbuild
A pre-existing DEFAULT_SESSION freezes credentials only. Region and AWS_SDK_UA_APP_ID re-resolve per client, because botocore's EnvironmentProvider holds a live os.environ reference.
Worth fixing rather than shrugging at, because the credential half is real and is the stronger argument: build-role credentials baked into a snapshot every MicroVM restores from is a security property; a stale region is a bug. This claim is the sole justification for three helpers and a deliberately awkward branch at server.py:1839 — a maintainer who tests the region claim, finds it false, and concludes the discipline was cargo-cult would delete helpers whose real rationale is sound.
Fix: narrow all three comments to credentials.
B5. GITHUB_TOKEN_SECRET_ARN redirect reads another workspace's OAuth token
agent/src/server.py:1074 + cdk/src/constructs/lambda-microvm-compute.ts:1168-1184
_install_platform_config validates keys rigorously and values not at all — no ARN-shape, account, partition, or region check before os.environ[env_name] = value. Most keys are contained by IAM (a foreign agent_session_role_arn AccessDenies → SessionScopingError → fails closed; tables/buckets are LeadingKeys/prefix-scoped). One is not:
The execution role holds GetSecretValue on bgagent-linear-oauth-* / bgagent-jira-oauth-*. config.py:55-65 fetches whatever ARN GITHUB_TOKEN_SECRET_ARN names and caches the raw SecretString into os.environ["GITHUB_TOKEN"], from which shell.py passes the environment to every repo subprocess — i.e. into the model's tool surface. So a /run payload naming another workspace's channel-OAuth secret succeeds: allowlisted key, unvalidated value, matching grant.
The prefix grant is at ECS parity. The asymmetry that makes it reachable is new: on ECS the ARN arrives as deploy-time container env; here it arrives in a network payload. The comment at server.py:874 ("secrets are still fetched at /run time … using the ARNs delivered here") is precisely where the ARN needs to stop being free-form.
Fix: pin *_secret_arn / *_role_arn values to the MicroVM's own partition/account/region before install, rejecting with the existing …_INVALID code. A per-key regex table beside env_by_key keeps it contract-sourced.
Related, and worth recording: NO_INGRESS reachability was never negatively verified. 645-p2-smoke-runbook.md:745-749 notes a NO_INGRESS VM still returns a public <vm-id>.lambda-microvm.<region>.on.aws hostname and warns that "endpoint exists" isn't evidence of reachability — but nobody probed it. /run has no application-layer auth (no Depends/HTTPBearer; lambda:CreateMicrovmAuthToken granted to no role, by design per sub-decision 3), so the entire posture rests on that untested inference. A bounded probe belongs in the P3 runbook.
B6. A real AWS account ID is committed
cdk/test/bootstrap/policies.test.ts:518-519 — 704224321915
Verified new in this PR (absent from main), against a repo-wide convention of 123456789012 (8 uses in the same tree). Both 2,000-line runbooks in this PR correctly redact to <account> — the runbooks were scrubbed, the test wasn't. Not a secret, but this is a public sample repo. The test asserts prefix-glob survival across CFN's 64-char truncation, which depends only on the role-name segment, so the placeholder loses nothing.
B7. The iam:PassRole map entries are inert, and the comment names a guard that doesn't exist
cdk/src/bootstrap/resource-action-map.ts:89-100
"listing the action here is what makes its removal a test failure instead of a redeploy failure."
It doesn't. collectBootstrapAllowActions() collects action strings only, discarding Resource and Condition — and infrastructure.ts's IAMPassRole already contributes bare iam:PassRole to every bundle. The requirement is therefore satisfied by the conditioned statement, which is exactly the one P2r2-F9 proved is denied on this path. Empirically confirmed: deleting MicrovmPassRoles leaves the check green (missing for MicrovmImage: []). Compounding it, synth-coverage.test.ts synthesizes only the default context, where AWS::Lambda::MicrovmImage is never emitted, so these entries are never consulted.
Not an open hole — the new policies.test.ts SID assertions do catch removal. But it's a security-relevant comment misdescribing which mechanism protects the fix.
Fix: point the comment at policies.test.ts, or make the map condition-aware.
Non-blocking
- Null byte → 500 instead of a structured 400.
server.py:1074. Verified:os.environ['X']='a\x00b'raisesValueError, which escapes_install_platform_config(raises only_PlatformConfigError) and bypasses the handler'sexceptat :1814. Newlines are accepted silently (log injection into your structured lines). Keys get regex validation; values don't. Untested. - MicroVM poll failures never escalate.
orchestrate-task.ts:405-415warns forever; the ECS sibling 40 lines up fails at 3 consecutive failures. A permanent fault (missinglambda:GetMicrovm, Region gap) is indistinguishable from a hiccup → ~1020 identical warns and a full 8.5 h billed reservation — the cost posture this PR's own heartbeat work exists to prevent. - Payload retention regresses ECS parity.
deleteEcsPayloadis called at finalize (orchestrate-task.ts:458); MicroVM has no equivalent and relies solely onMICROVM_PAYLOAD_TTL_DAYS = 1. With bucket-widegrantReadand<taskId>/payload.jsonkeys, any running MicroVM can read another task's hydrated prompt/issue thread — window widens from minutes to ~24 h. - ADR-021's own EARS requirement is unimplemented. :52 requires CSPRNG reseeding on
/run; :378 calls missing it "a silent security defect." Norandom.seed/os.urandomanywhere inagent/src. Exposure is small (onlyprogress_writer.py:75'sgetrandbits(80)ULID, a sort key under atask_idpartition — a collision needs same task and millisecond). Implement the one-liner or amend the ADR. - Unreachable
FINALIZINGarm.finalizeTaskacts onsessionUnhealthyfor RUNNING or FINALIZING, butpollTaskStatuscan only set it for RUNNING — dead for all three backends. _debug_cw_failuresis incremented, never read. Three docstrings describe an alarm operators can watch; no metric, alarm, or/pingexposure exists. Pre-existing — but B4's rationale leans on it ("would poison the signal"), and there's no signal to poison./validatesecret detection reports into a void.server.py:1629.warnings: ["secret_env_present_in_snapshot:GITHUB_TOKEN"]rides a 200 nothing parses. One_build_hook_logline would land it in the build log group. Report-only is the right call; discarding it isn't._READY_WARMUP_TOTAL_BUDGET_SECONDS: float = 240.0evades the drift regex (\d+\bfails on240.0), andcheck-constants-sync.tsgained ~140 lines with no test file anywhere.- Producer misattributed.
server.py:879sends readers toorchestrator.tsfor theplatform_configproducer; it'sbuildMicrovmPlatformConfigin the strategy (grep finds 0 hits inorchestrator.ts). agent_heartbeat_atontoTaskDetailbut nottoTaskSummary—bgagent liststill can't show liveness. Worth a deliberate decision given the PR's own framing.logs:CreateLogGroupon the execution role (:1482) — the group is pre-created at:1018, so this is a create right the runtime never uses on the role that runs untrusted repo code. Splitting build/runtime grants costs three lines.- PR description understates the artifacts. The body says the smoke run "executes against a live account after this lands," while ADR-021:403 and the synth warning both record run 2 passing on 2026-08-07 (2 PRs, 12 turns, $0.279, heartbeat observed). The artifacts are more accurate than the description — worth reconciling so the honest scope note is trusted.
Vision alignment — strongly aligned
- Bounded blast radius —
platform_configremoves the image snapshot as a source of deployment truth. Rejecting the whole block on an unknown key rather than filtering correctly treats unrecognized keys asLD_PRELOAD/AWS_ENDPOINT_URLinjection, not a compatibility gap. I verified the 13-key allowlist contains no process-hijacking variable. - Bounded cost — heartbeat liveness on
lambda-microvmcloses a real 8-hour-reservation burn; live evidence shows a hung guest sits inRUNNINGindefinitely with nostateReason. - Reviewable outcomes — the unconditional, non-suppressible synth warning telling operators to keep production on
agentcore/ecsis exactly right, as is freezing the warning id across phases. - Tenet trades documented — the dropped
iam:PassedToService/aws:SourceAccountconditions are evidenced by a controlled two-arm experiment with per-role compensating controls in ADR-021 §4. The ADR retracts its own earlier false negative and diagnoses the contaminated control that caused it. That's the standard.
Documentation — complete
- Mirror in sync (verified): ran
node scripts/sync-starlight.mjs→ exit 0,git statusclean. - COMPUTE.md adds the Lambda MicroVMs column and the explicit classic-Lambda distinction (
COMPUTE.md:24) — the #645 acceptance criterion, satisfied. - ORCHESTRATOR.md, SECURITY.md (all three roles), DEPLOYMENT_ROLES.md golden baseline,
contracts/constants.md, ADR-021 amended asproposed. - Gap: the mandatory re-bootstrap to ≥1.4.0 appears in the ADR, the synth warning, and the packaging script — but no
docs/guides/file mentionslambda-microvmat all. An operator following a guide won't learn they must re-bootstrap.
Tests & CI
CI 4/4 green. cdk 3,825 / agent 1,590 (82.8%).
Bootstrap synth-coverage: PASS, verified by running it. Reframing worth noting: the constructs pre-existed this PR, so there are no new CFN resource types — the addition is one MicrovmPassRoles statement. A context-enabled synth showed UNMAPPED TYPES: [], MISSING ACTIONS: {}. Artifact regeneration was byte-identical (no hand-editing). 1.3.0 → 1.4.0 is the correct minor bump. test/bootstrap: 113/113. The ARN-truncation risk is pinned by a test, not just a comment — ConnectorOperatorRole truncates to …ComputeConnectorOp-, still inside the glob, and the modeled BuildRoleF0 truncation reproduces the live ARN quoted in the comment.
Rule #366 (synth perf): not violated — no test re-enables bundling; new synths are beforeAll-cached.
The poisoned-seam suite is real, not theater. It sets LOG_GROUP_NAME before asserting silence (without which every assertion passes trivially), and install_phase_done flips only on a non-empty install list — the difference between a real test and a hole, since _install_platform_config(None) returning [] would otherwise disarm the guard across the exact legacy path where the bug lived. Mutation claims spot-checked and held: reverting _build_hook_log→_debug_cw, platform_client→boto3.client, and heartbeatLivenessApplies→=== 'agentcore' are each caught.
Real gaps: no \x00/\n value test; /terminate's active: None branch untested despite a comment arguing it's load-bearing; and the heartbeat→TerminateMicrovm composition is unasserted for any backend — fakeContext ignores waitStrategy, so the billing outcome the change exists for isn't covered end-to-end. The runbook admits the switch "never got a long-enough RUNNING window to exercise."
Review agents run
All seven dispatched and completed: code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer, plus security/IAM and bootstrap-coverage reviews. None omitted.
Two agents disagreed on B4's boto3.DEFAULT_SESSION behavior, so I settled it by experiment rather than vote — and my first five measurements all agreed with the wrong answer because ambient AWS_PROFILE/~/.aws/config silently confounded them, S3 masks region behind a us-east-1 fallback, and this botocore reads AWS_DEFAULT_REGION rather than AWS_REGION. Same shape as the contaminated control ADR-021 documents and retracts. Flagging because it means any reviewer re-checking B4 needs a clean-room env to see it.
Human heuristics
- Proportionality — concern.
server.py1,892 lines,lambda-microvm-compute.ts1,534, ~4,100 lines of in-tree runbooks. Most length is load-bearing evidence (measured values, the probe that established them, the wrong value replaced) — genuinely unrecoverable once lost. But two ~30-line docstrings document an "unreachable in practice today" branch twice (server.py:1550,1600), and phase-status prose is narrated in three files needing hand-updates per phase. - Coherence — pass. Correct routing. New backend reuses
ComputeStrategyunchanged (verified: empty diff oncompute-strategy.ts) — no optional member forced on the other two. Divergences fromecs-strategy(NotFound→completed) are explained by substrate behavior, not convenience. - Clarity — concern. Names are good; the exhaustive
neverswitch is mutation-verified compile-breaking. But B1/B2 hide real causes behind plausible defaults (AI004), and B3/B4/B7 are comments asserting mechanisms that don't exist — the highest-cost comment defect, since maintainers act on them. - Appropriateness — mostly pass. Verified against real AWS behavior, not self-written mocks (AI001) — the 4,096-vs-documented-16,384
runHookPayloadcap with its verbatimValidationExceptionis exactly the grounding I want, as is the shell-parsing test that replaced a prose "keep these in step" comment which had already failed once. Two exceptions: the inner/outerplatform_configprecedence tests pin a branch the producer's own contract makes unobservable, andcontracts/constants.jsonderivation is rightly preferred over hand-copied literals.
Genuinely good work — the live-evidence discipline, the self-retracting ADR, and the poisoned-seam suite are all above the bar for this repo. The gate is narrow: make the failures say what actually happened (B1, B2), fix three comments that would mislead a maintainer into breaking ECS or deleting sound credential hygiene (B3, B4, B7), pin ARN values (B5), and scrub the account ID (B6).
scottschreckengaust
left a comment
There was a problem hiding this comment.
Inline follow-up to my review above — the same findings anchored to specific lines, with committable suggestion blocks where the fix is unambiguous.
Directly committable via the GitHub UI: B6 (account ID), B3 (heartbeat docstring), B4 (credential-vs-region wording), B7 (map-entry comment), and the control-character nit.
Code sketches rather than one-click suggestions — because they touch a type or span non-adjacent lines: B1 (stateReason needs an optional reason?: string on SessionStatus plus the orchestrator.ts:451 detail string), B2 (wrapMicrovmError — verify the escaped apostrophe survives your lint), and B5 (ARN pinning).
B1 is anchored slightly above pollSession because that function falls outside the diff hunks; the comment names the real line range.
Everything asserted here I verified by running it — the classifier regexes, the botocore region/credential behavior in this repo's own venv, the os.environ null-byte ValueError, and the bootstrap action-collection check with MicrovmPassRoles deleted.
| 'arn:aws:iam::704224321915:role/backgroundagent-dev-LambdaMicrovmComputeBuildRoleF0-9FxjQbiJC3px', | ||
| 'arn:aws:iam::704224321915:role/backgroundagent-dev-LambdaMicrovmComputeConnectorOp-Ab12Cd34Ef56', |
There was a problem hiding this comment.
B6 (blocking, 30-second fix): a real 12-digit AWS account ID in a public sample repo.
704224321915 is new in this PR (absent from main), against a repo-wide convention of 123456789012 — 8 uses elsewhere in cdk/test. Both 2,000-line runbooks in this same PR correctly redact to <account>: the runbooks were scrubbed, this test wasn't.
The assertion's purpose — that the prefix globs survive CFN's 64-char logical-id truncation — depends only on the role-name segment, so the placeholder loses nothing.
| 'arn:aws:iam::704224321915:role/backgroundagent-dev-LambdaMicrovmComputeBuildRoleF0-9FxjQbiJC3px', | |
| 'arn:aws:iam::704224321915:role/backgroundagent-dev-LambdaMicrovmComputeConnectorOp-Ab12Cd34Ef56', | |
| 'arn:aws:iam::123456789012:role/backgroundagent-dev-LambdaMicrovmComputeBuildRoleF0-9FxjQbiJC3px', | |
| 'arn:aws:iam::123456789012:role/backgroundagent-dev-LambdaMicrovmComputeConnectorOp-Ab12Cd34Ef56', |
| if (missing.length > 0) { | ||
| throw new Error( | ||
| 'Cannot start a lambda-microvm session: the orchestrator environment is missing platform ' | ||
| + `configuration the in-guest agent cannot run without (${missing | ||
| .map(key => `${key} <- ${PLATFORM_CONFIG_ENV_VARS[key]}`) | ||
| .join(', ')}). A MicroVM snapshot must not bake these in (ADR-021 sub-decision 3), so the ` | ||
| + '/run payload is the only channel for them. TaskOrchestrator injects every one of these ' | ||
| + 'from stack-level values, so this indicates the orchestrator function\'s environment was ' | ||
| + 'edited outside CDK — redeploy the stack to restore it.', | ||
| ); |
There was a problem hiding this comment.
B2 (blocking): this throw escapes without MICROVM_ERROR_MARKER, so it is classified TRANSIENT and auto-retried with a remedy naming the wrong substrate.
I ran the real classifier patterns against this message. It matches none of the MicroVM entries (they require MicroVM [\w ]+failed), so it falls through to the generic catch-all at error-classifier.ts:320:
pattern: /Session start failed/i
remedy: "Check AgentCore Runtime or ECS cluster health."
retryable: true
So a hand-edited-Lambda-environment misconfiguration — which retrying provably cannot fix — gets retried, and the operator is pointed at AgentCore/ECS for a lambda-microvm fault. This file's own marker docstring calls the marker "mandatory, not decorative" for exactly this reason.
Definitive fix — reuse the existing wrapper so the marker and cause chain both land:
| if (missing.length > 0) { | |
| throw new Error( | |
| 'Cannot start a lambda-microvm session: the orchestrator environment is missing platform ' | |
| + `configuration the in-guest agent cannot run without (${missing | |
| .map(key => `${key} <- ${PLATFORM_CONFIG_ENV_VARS[key]}`) | |
| .join(', ')}). A MicroVM snapshot must not bake these in (ADR-021 sub-decision 3), so the ` | |
| + '/run payload is the only channel for them. TaskOrchestrator injects every one of these ' | |
| + 'from stack-level values, so this indicates the orchestrator function\'s environment was ' | |
| + 'edited outside CDK — redeploy the stack to restore it.', | |
| ); | |
| throw wrapMicrovmError( | |
| 'platform config', | |
| new Error( | |
| 'the orchestrator environment is missing platform configuration the in-guest agent ' | |
| + `cannot run without (${missing | |
| .map(key => `${key} <- ${PLATFORM_CONFIG_ENV_VARS[key]}`) | |
| .join(', ')}). A MicroVM snapshot must not bake these in (ADR-021 sub-decision 3), so ` | |
| + 'the /run payload is the only channel for them. TaskOrchestrator injects every one of ' | |
| + 'these from stack-level values, so this indicates the orchestrator function\'s ' | |
| + 'environment was edited outside CDK - redeploy the stack to restore it.', | |
| ), | |
| ); |
Please also add a test asserting the classification, not just the message text — the current tests at lambda-microvm-strategy.test.ts:663-684 would pass either way, which is why this slipped. Note assertImageArn and the substrate-missing throw above have the same gap; both predate this PR, so I'm not gating on them.
| platform_config_keys: Object.keys(platformConfig), | ||
| }); | ||
|
|
||
| return { |
There was a problem hiding this comment.
B1 (blocking): stateReason is discarded, so the most common runtime failure reports a fabricated cause.
Anchoring here because pollSession (~:691-744) is outside the diff hunks.
pollSession reads result.state and ignores stateReason, which GetMicrovmResponse carries. This PR's own runbook (docs/verification/645-p2-smoke-runbook.md:771) records what that field says on the dominant failure mode:
state = TERMINATED
stateReason = Run lifecycle hook returned HTTP status 400. Please check your hook endpoint...
TERMINATED -> { status: 'completed' } has no error slot, so orchestrator.ts:451 builds detail = "substrate state completed" and the operator sees:
MicroVM substrate terminated before the agent wrote a terminal status: substrate state completed
…with a classifier remedy citing "session duration cap, host fault, or an external terminate" — none of which happened. Every distinct wire code /run carefully emits (MICROVM_RUN_PLATFORM_CONFIG_INVALID, MICROVM_RUN_PAYLOAD_UNREADABLE, TASK_RECORD_INCOMPLETE) is discarded one layer up. The runbook also notes this path self-terminates in ~12 s, so it is the fast, common case — not an edge.
Per this repo's standard, a plausible-but-wrong result is a defect.
Minimal fix (no type change) — capture it in pollSession and surface it:
// in pollSession, after `state = result.state;`
const stateReason = result.stateReason;
// ...
case MicrovmState.TERMINATING:
case MicrovmState.TERMINATED:
if (stateReason && stateReason !== 'Success.') {
logger.warn('MicroVM terminated with a substrate reason', {
microvm_id: microvmId, state, state_reason: stateReason,
});
}
return { status: 'completed', reason: stateReason };Then widen SessionStatus with an optional reason?: string and append it at orchestrator.ts:451:
const detail = substrate.status === 'failed'
? substrate.error
: `substrate state ${substrate.status}${substrate.reason ? ` (${substrate.reason})` : ''}`;That single change turns the misleading message into substrate state completed (Run lifecycle hook returned HTTP status 400…), which points the operator at the guest logs where your structured 400 body already is.
| * Whether a backend's liveness is (partly) inferred from `agent_heartbeat_at`. | ||
| * | ||
| * The agent writes that timestamp UNCONDITIONALLY on every substrate — it is a | ||
| * DynamoDB write from the pipeline, with no backend awareness — so this predicate | ||
| * decides only whether the ORCHESTRATOR acts on it. |
There was a problem hiding this comment.
B3 (blocking): this comment asserts an invariant the code does not have — and the error direction breaks ECS.
"The agent writes that timestamp UNCONDITIONALLY on every substrate … so this predicate decides only whether the ORCHESTRATOR acts on it."
Verified against the code, and it is not true:
agent/src/pipeline.py:919writes the timestamp once, right afterwrite_running. Backend-agnostic as claimed, but it never repeats.- The only periodic writer is
_heartbeat_worker(agent/src/server.py:272, 45 s cadence), started solely from_run_task_background— reachable only via/invocations(AgentCore) or the MicroVM/runhook. - ECS bypasses the server entirely.
ecs-strategy.ts:245: "This bypasses the uvicorn server entirely — no HTTP, no OTEL noise."
So on ECS agent_heartbeat_at is written once and never refreshed.
Why the wording matters: it frames ecs => false as a tuning preference ("two independently-tuned kill paths") when it is a hard correctness constraint. Anyone acting on this comment to consolidate those paths would flip ecs to true and fail every ECS task after ~6 minutes (AGENT_HEARTBEAT_GRACE_SEC 120 + AGENT_HEARTBEAT_STALE_SEC 240). The comment invites the change that breaks the backend.
| * Whether a backend's liveness is (partly) inferred from `agent_heartbeat_at`. | |
| * | |
| * The agent writes that timestamp UNCONDITIONALLY on every substrate — it is a | |
| * DynamoDB write from the pipeline, with no backend awareness — so this predicate | |
| * decides only whether the ORCHESTRATOR acts on it. | |
| /** | |
| * Whether a backend's liveness is (partly) inferred from `agent_heartbeat_at`. | |
| * | |
| * The *periodic* writer is `_heartbeat_worker` (`agent/src/server.py`, 45 s), which | |
| * runs ONLY on substrates that go through the FastAPI server — `/invocations` | |
| * (AgentCore) and the `/run` hook (MicroVMs). `pipeline.py` also writes the | |
| * timestamp once at RUNNING on every substrate, but that single write goes stale by | |
| * design, so this predicate decides which backends may treat staleness as signal. |
Please also retitle the ecs bullet below to lead with the constraint rather than the preference — e.g. "ecs — no, and NOT a tuning choice: the ECS boot command bypasses uvicorn, so _heartbeat_worker never starts and the timestamp is written exactly once. Enabling this would fail every ECS task after ~6 min (grace + stale)." That also makes the never guard genuinely useful: a fourth backend's correct answer then turns on a checkable fact — does it serve HTTP? — rather than taste.
| ``_debug_cw`` / ``_warn_cw``: those writers spawn a daemon thread that builds a | ||
| CloudWatch Logs client whenever ``LOG_GROUP_NAME`` is set, which drags in AWS | ||
| credential resolution and — worse — populates ``boto3.DEFAULT_SESSION``, a | ||
| module global holding a resolved credential chain plus the region that was | ||
| current when it was created. |
There was a problem hiding this comment.
B4 (blocking): the region half of this mechanism is factually wrong, and it is the load-bearing half as written.
I tested this in this repo's own venv (botocore 1.43.42), controlling for ambient AWS_PROFILE / ~/.aws/config:
BUILD session.region_name: us-west-2 | logs client: us-west-2
AFTER session.region_name: eu-west-1 | logs client: eu-west-1
creds frozen to build role: AKIAbuild
A pre-existing boto3.DEFAULT_SESSION freezes credentials only. Region and AWS_SDK_UA_APP_ID are re-resolved from os.environ on every client construction, because botocore's EnvironmentProvider holds a live reference to os.environ (configprovider.py) and ConfigValueStore.get_config_variable calls provider.provide() per lookup. Credentials are memoized (botocore/session.py: if self._credentials is None).
This is worth correcting rather than shrugging at, because the true statement is the stronger one: build-role credentials frozen into a snapshot that every MicroVM restores from is a security property; a stale region is merely a bug. And this claim is the sole justification for three helpers plus the deliberately awkward branch at server.py:1839 — a maintainer who checks the region claim, finds it false, and concludes the whole AWS-silence discipline was cargo-cult would delete helpers whose credential rationale is real and correct.
| ``_debug_cw`` / ``_warn_cw``: those writers spawn a daemon thread that builds a | |
| CloudWatch Logs client whenever ``LOG_GROUP_NAME`` is set, which drags in AWS | |
| credential resolution and — worse — populates ``boto3.DEFAULT_SESSION``, a | |
| module global holding a resolved credential chain plus the region that was | |
| current when it was created. | |
| hooks, and ``/run`` before ``platform_config`` is installed). Deliberately NOT | |
| ``_debug_cw`` / ``_warn_cw``: those writers spawn a daemon thread that builds a | |
| CloudWatch Logs client whenever ``LOG_GROUP_NAME`` is set, which drags in AWS | |
| credential resolution and — worse — populates ``boto3.DEFAULT_SESSION``, a | |
| module global that MEMOIZES the resolved credentials for the life of the process | |
| (``botocore.session.Session.get_credentials`` loads once, then caches). Region | |
| and ``AWS_SDK_UA_APP_ID`` are NOT frozen — botocore re-reads those from | |
| ``os.environ`` per client — but the credentials are, and on a build hook those | |
| are the BUILD role's. |
Two sibling comments need the same narrowing: _build_hook_log reason 2 (:1105-1107) should read "(the build role's credentials — region is re-resolved per client, credentials are not)", and :1122-1123) should drop "region, _pre_config_log (AWS_SDK_UA_APP_ID" and keep the real defect — pinning credentials resolved before AGENT_SESSION_ROLE_ARN is installed.
Note the surrounding argument survives fully intact, and _fetch_microvm_payload_from_s3's "platform_client does not touch the cached session" is already consistent and correct.
| installed: list[str] = [] | ||
| for key, value in sorted(resolved.items()): | ||
| env_name = MICROVM_PLATFORM_CONFIG_ENV_BY_KEY[key] | ||
| os.environ[env_name] = value |
There was a problem hiding this comment.
B5 (blocking): values reach os.environ unvalidated, and one allowlisted key lets a /run payload read another workspace's OAuth token.
The key allowlist here is genuinely well built — fails closed on unknown keys rather than filtering, rejects non-string values, enforces the 4 required keys, and I verified the 13 permitted env names contain no process-hijacking variable (PATH, LD_PRELOAD, AWS_ENDPOINT_URL, AWS_ACCESS_KEY_ID, HTTPS_PROXY, NODE_OPTIONS, ANTHROPIC_BASE_URL — zero overlap). Values, though, are installed verbatim: no ARN-shape, partition, account, or region check anywhere on this path.
I traced each key against what the execution role's IAM actually permits. Most are contained: a foreign agent_session_role_arn AccessDenies (sts:AssumeRole is granted on the one SessionRole ARN) and aws_session.get_session raises SessionScopingError — fails closed, no silent downgrade to ambient credentials. Tables and buckets are LeadingKeys/prefix-scoped to the caller's own task_id/user_id.
One is not contained:
lambda-microvm-compute.ts:1168-1184 grants the execution role secretsmanager:GetSecretValue on bgagent-linear-oauth-* and bgagent-jira-oauth-*. agent/src/config.py:55-65 fetches whatever ARN GITHUB_TOKEN_SECRET_ARN names and caches the raw SecretString into os.environ["GITHUB_TOKEN"] — from which shell.py passes the environment to every repo subprocess, i.e. into the model's tool surface. A /run payload naming another workspace's channel-OAuth secret therefore succeeds: allowlisted key, unvalidated value, matching grant.
The prefix grant itself is at ECS parity. The asymmetry that makes it reachable is new to this backend: on ECS the secret ARN arrives as deploy-time container env; here it arrives in a network payload. The block comment above ("secrets are still fetched at /run time from Secrets Manager using the ARNs delivered here") is exactly where the ARN must stop being free-form.
Suggested fix — pin ARN-shaped values to this MicroVM's own partition/account/region before install, reusing the existing failure code:
# ARN-shaped values are installed into the env that resolves credentials and
# fetches secrets, so a payload-supplied ARN pointing outside THIS deployment is
# an exfiltration primitive, not a config choice (the execution role holds a
# prefix grant on the channel-OAuth secrets). Pin partition/account/region.
self_arn = os.environ.get("AGENT_SESSION_ROLE_ARN", "")
bad_arns: list[str] = []
for key, value in resolved.items():
if not (key.endswith("_arn")):
continue
parts = value.split(":", 5)
if len(parts) < 6 or parts[0] != "arn":
bad_arns.append(f"{key}:malformed")
continue
if self_arn:
mine = self_arn.split(":", 5)
# partition + account must match; region may be empty for global services
if parts[1] != mine[1] or parts[4] != mine[4]:
bad_arns.append(f"{key}:foreign-account-or-partition")
if bad_arns:
raise _PlatformConfigError(
"MICROVM_RUN_PLATFORM_CONFIG_INVALID",
"platform_config ARN values must name resources in this deployment's own "
f"partition and account; rejected {sorted(bad_arns)}",
)A per-key regex/validator table beside env_by_key in contracts/constants.json would be the cleaner long-term shape, keeping it contract-sourced like the rest.
Related, and worth recording in the runbook rather than fixing here: /run has no application-layer auth (no Depends/HTTPBearer; lambda:CreateMicrovmAuthToken is granted to no role, deliberately per sub-decision 3), so the whole posture rests on NO_INGRESS. Your own runbook (645-p2-smoke-runbook.md:745-749) notes a NO_INGRESS VM still returns a public <vm-id>.lambda-microvm.<region>.on.aws hostname and warns that "endpoint exists" is not evidence of reachability — and nobody probed it. A bounded negative probe (curl the hostname from outside, expect connection failure) belongs in P3. This finding is what that probe protects.
| continue | ||
| if value.strip(): | ||
| resolved[key] = value | ||
|
|
There was a problem hiding this comment.
Non-blocking (untested failure path): a null byte in any value escapes as a 500 instead of your structured 400.
Verified in Python 3: os.environ['X'] = 'a\x00b' raises ValueError: embedded null byte. That is not a _PlatformConfigError, so it escapes _install_platform_config, bypasses the handler's except _PlatformConfigError at ~:1814, and becomes a FastAPI 500 — the one place this module doesn't produce the structured body it is otherwise careful about. A newline is accepted silently, which is a log-injection vector into the structured lines you emit.
Note the keys get regex validation (_PLATFORM_CONFIG_KEY_RE) and the values don't, and the tests cover non-string (test_server.py:1917) and blank (1925/1935) but no control character. Since this block is explicitly framed as injection defense, value content is the other half of the same threat model.
| continue | |
| if value.strip(): | |
| resolved[key] = value | |
| if not isinstance(value, str): | |
| bad_types.append(f"{key}:{type(value).__name__}") | |
| continue | |
| if "\x00" in value or "\n" in value or "\r" in value: | |
| # These become process env vars: a NUL raises ValueError from os.environ | |
| # (a 500 instead of this module's structured 400), and a newline forges | |
| # extra lines in the structured logs emitted below. | |
| bad_types.append(f"{key}:control-characters") | |
| continue | |
| if value.strip(): | |
| resolved[key] = value |
| // the conditional `compute-lambda-microvm` policy; listing the action here is | ||
| // what makes its removal a test failure instead of a redeploy failure. Evidence | ||
| // inlined in ADR-021 §4. | ||
| 'AWS::Lambda::MicrovmImage': ['lambda:CreateMicrovmImage', 'iam:PassRole'], | ||
| 'AWS::Lambda::NetworkConnector': ['lambda:CreateNetworkConnector', 'iam:PassRole'], |
There was a problem hiding this comment.
B7 (important): these entries are inert, and the comment above claims a guard that does not exist.
"listing the action here is what makes its removal a test failure instead of a redeploy failure."
It doesn't. collectBootstrapAllowActions() in this same file collects action strings only — Resource and Condition are discarded. infrastructure.ts's IAMPassRole statement already contributes the bare action iam:PassRole to every bundle, so this requirement is satisfied by the conditioned statement — precisely the statement P2r2-F9 proved is denied on this path.
Confirmed empirically with a throwaway test: with MicrovmPassRoles deleted, the check stays green —
missing for MicrovmImage: []
has iam:PassRole: true
Compounding it: synth-coverage.test.ts synthesizes only the default context, where AWS::Lambda::MicrovmImage is never emitted, so these two entries are never even consulted.
This is not an open hole — removal is caught, by your new policies.test.ts assertions on the SID list and the absent condition. So the fix is to stop the comment pointing at the wrong mechanism, since a future maintainer will read it when deciding whether these entries are load-bearing:
| // the conditional `compute-lambda-microvm` policy; listing the action here is | |
| // what makes its removal a test failure instead of a redeploy failure. Evidence | |
| // inlined in ADR-021 §4. | |
| 'AWS::Lambda::MicrovmImage': ['lambda:CreateMicrovmImage', 'iam:PassRole'], | |
| 'AWS::Lambda::NetworkConnector': ['lambda:CreateNetworkConnector', 'iam:PassRole'], | |
| // the conditional `compute-lambda-microvm` policy. NOTE: listing the action here | |
| // does NOT guard that statement — `collectBootstrapAllowActions` compares action | |
| // STRINGS only (Resource/Condition are discarded), and `infrastructure`'s | |
| // conditioned `IAMPassRole` already satisfies a bare `iam:PassRole` requirement. | |
| // The real guard against dropping the unconditioned pass is | |
| // `test/bootstrap/policies.test.ts` ("MicrovmPassRoles"), which asserts the sid, | |
| // the two name-prefix resources, the absent condition, and the execution-role | |
| // exclusion. These entries document the create-time need; they do not enforce it. |
If you'd rather make the map genuinely enforce it, findMissingBootstrapActions would need to require an unconditioned match for a declared subset of actions — a bigger change I would not gate this PR on.
ayushtr-aws
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Second-pass review (principal-architect lens + the mandatory pr-review-toolkit agents, run against the checked-out branch). I re-verified @scottschreckengaust's earlier review against current HEAD and found some new issues; this comment does not restate B1–B7 — it confirms they're still live and adds three findings that review didn't cover.
Excellent, evidence-disciplined work overall — platform_config as deployment-truth-off-the-snapshot, the poisoned-seam suite, and the self-retracting ADR are all above this repo's bar. The gate is the same theme the first review named: this PR fails closed correctly, then loses or misreports why it failed.
Prior blockers are all still open
The earlier review landed on 08b3091, which is still HEAD — no fix commits since — so B1–B7 remain unaddressed. I independently reproduced every one; flagging that they still gate:
- B1
pollSession(lambda-microvm-strategy.ts:696) discardsstateReason→ dominant failure renders"substrate state completed". - B2 required-key throw (
lambda-microvm-strategy.ts:283) skipswrapMicrovmError— with a correction to the earlier framing (see below). - B3
orchestrator.ts:84-86— confirmed false;ecs => falseis a hard correctness constraint (see N-follow-up below). - B4
boto3.DEFAULT_SESSIONfreezes credentials only, not region. - B5
_install_platform_configvalidates key allowlist + value type, never value content → cross-workspace OAuth-secret ARN redirect. - B6 real account ID
704224321915atpolicies.test.ts:518-519. - B7
resource-action-map.ts:96-97— theiam:PassRolemap entry is inert; the real guard ispolicies.test.ts.
New blocking-grade findings (not in the prior review)
N1 — S3 payload parse errors are misclassified as non-retryable 400. server.py:1246-1248: on the dominant pointer path, json.loads(body) (JSONDecodeError ⊂ ValueError) and the non-object guard raise bare ValueError, which hits /run's except ValueError → 400 MICROVM_RUN_PAYLOAD_INVALID ("retrying an identical body cannot help") before the except Exception → 500 …_UNREADABLE ("retrying CAN help"). A truncated / racing / half-written S3 object is retryable, and the operator is told the orchestrator built a bad envelope — it didn't; the S3 object was bad. Only the pre-fetch URI-shape ValueError at :1239 correctly belongs in the 400 branch, which is exactly why a blanket except ValueError is the wrong discriminator.
Fix: raise a dedicated non-ValueError (e.g. PayloadFetchError) for post-fetch content problems so they fall through to the 500 branch. Note the function-level ValueError on a non-object body is covered (test_server.py:1461), but no test drives that unreadable body through the /run handler to assert its classification — the handler-level S3-failure test (test_server.py:1464) mocks _fetch to raise RuntimeError, so the JSONDecodeError/non-object → 400 misroute is never exercised end-to-end.
N2 — the no-platform_config path silently skips the required-key check, so tenant isolation can be silently OFF. server.py:1025-1026: _install_platform_config(None) returns [] before the required-key validation at :1061. agent_session_role_arn is a required key precisely because aws_session downgrades to ambient compute-role credentials with tenant scoping silently off when AGENT_SESSION_ROLE_ARN is unset, and ADR-021 sub-decision 3 forbids baking that ARN into the snapshot. The docstring itself says the image and orchestrator "deploy on independent cadences" — so a pre-P2 orchestrator (no platform_config sibling) launching a P2 image produces a task that runs to completion with per-tenant isolation disabled, traced only by a stdout-only _pre_config_log line that never reaches the task's CloudWatch group. This is the sharpest failure the required-key list exists to prevent, and the None path routes around it.
Fix: on the None path, still enforce MICROVM_PLATFORM_CONFIG_REQUIRED_KEYS against the effective environment (payload-or-baked) and reject …_INCOMPLETE when a required key is neither delivered nor present; at minimum escalate the no-config branch beyond a stdout breadcrumb so a silently-unscoped run is auditable.
N3 — comment/code contradiction on the build-role Logs grant. server.py:1102-1104 states "The build role has no Logs grant, so the write can only FAIL" — but lambda-microvm-compute.ts:1094 calls grantMicrovmLogWrites(this.buildRole), granting scoped logs:CreateLogGroup/CreateLogStream/PutLogEvents on the /aws/lambda-microvms/* namespace. The accurate statement is that the grant is scoped to the service namespace, so a write to any other LOG_GROUP_NAME (e.g. a baked APPLICATION_LOGS group) fails. Reason #2 in that same docstring (boto3.DEFAULT_SESSION freezing the build session into the snapshot) is accurate and load-bearing, so the code behavior is fine — only the stated IAM fact is wrong, and it's the kind a maintainer would act on.
Follow-ups reinforcing the prior review
-
B2 — correction to the earlier framing (I traced the classifier paths). The earlier review said the missing-key error is "classified TRANSIENT and auto-retried." The auto-retry half doesn't hold:
startSessionWithRetryclassifies the raw error (session-start-retry.ts:116, deliberately, per #599 — the/Session start failed/iwrapper is itself a transient pattern), and the raw "Cannot start a lambda-microvm session: …" matches no transient pattern → falls toUNKNOWN→USER→ thrown immediately, not retried. But the wrong-remedy half is real:failTaskpersists"Session start failed: <raw>", and the operator/channel-facingfailure-reply.ts:150re-classifies that prefixed string, which now matches the/Session start failed/iTRANSIENT catch-all → the user is told "Check AgentCore Runtime or ECS cluster health / quota exhausted" for a hand-edited-orchestrator-env fault on the MicroVM path. So B2 still stands as a real defect (misattributed cause +errorClass: TRANSIENT), just via the failure-reply classification rather than an actual retry.wrapMicrovmError('platform config', …)still fixes it; the fix and its blocking status are unchanged. -
B3 is a hard correctness constraint, restated for emphasis.
pipeline.py:919writes the heartbeat once; the periodic 45s refresh that staleness detection depends on is_heartbeat_worker(server.py:272), started only inside_run_task_background— the AgentCore/invocationsand MicroVM/runpaths. ECS launchesrun_task_from_payloaddirectly (ecs-strategy.ts:246), bypassingserver.py, so it has no periodic writer. A maintainer flippingecstotrueon the strength of the "predicate only decides whether the orchestrator acts" comment would fail every ECS task ~240s in. Please lead theecsbullet with the constraint.
Non-blocking nits (concur with the prior review; spot-verified)
- Null-byte value → uncaught
ValueError→ 500 + partial env install (fail-closed contract broken); newlines accepted silently (log injection).server.py:1074. Untested. - MicroVM payload has no
deleteEcsPayloadequivalent — relies solely onMICROVM_PAYLOAD_TTL_DAYS = 1; parity regression vs ECS (orchestrate-task.ts:458), widening the cross-task read window from minutes to ~24h given bucket-widegrantRead. - ADR-021's CSPRNG-reseed EARS requirement is unimplemented (no
random.seed/os.urandominagent/src; onlyprogress_writer.py:75). Implement the one-liner or amend the ADR. - Unreachable
FINALIZINGarm in thesessionUnhealthygate (orchestrator.ts:990) —pollTaskStatusonly sets it for RUNNING. - Heartbeat-stale failure message hard-codes "container" (
finalizeTask), now emitted for a MicroVM substrate that has none — parameterize bycomputeType. formatTaskDetailheartbeat age callsDate.now()directly (cli/src/format.ts), non-deterministic under test, unlike itsformatStatusSnapshotsibling which threads an injectednow.
Docs
Mirror verified in sync; COMPUTE.md's Lambda MicroVMs column + classic-Lambda distinction satisfies the #645 criterion. One gap stands from the prior review: the mandatory re-bootstrap to bundle ≥1.4.0 appears in the ADR, the synth warning, and the packaging script, but no docs/guides/ file mentions lambda-microvm — an operator following a guide won't learn they must re-bootstrap.
Tests & CI
CI 4/4 green. Bootstrap synth-coverage PASS / no new CFN types (constructs pre-existed; 1.3.0→1.4.0 is the correct minor bump). Rule #366 not violated (bundling stays off; new synths beforeAll-cached; the two per-test synths carry explicit constructor-throw exemption comments). Real gaps: no \x00/\n value test; /terminate's active:None branch is never actually exercised (the best-effort-failure test assigns active before the patched raise); and the heartbeat → TerminateMicrovm composition is unasserted end-to-end for any backend.
Review agents run
All five pr-review-toolkit agents dispatched against the checked-out branch and completed: code-reviewer (no ≥80-confidence blockers; confirmed type-sync of agent_heartbeat_at across cdk/cli, UA attribution #319 on the S3 fetch, bootstrap regen consistency, contract sourcing), silent-failure-hunter (N1, N2), comment-analyzer (N3, confirmed B3/B7), pr-test-analyzer (test gaps above). type-design-analyzer omitted — the diff adds no substantive new types (only optional reason? / agent_heartbeat_at fields on existing records, verified type-synced). Security/IAM + bootstrap judgment applied by hand.
Net: the seven prior blockers stand, and I'd add N1 (retryability inversion on the dominant payload path) and N2 (silent tenant-isolation-off on version skew) as blocking-grade, plus N3 as a must-fix misleading comment. The live-evidence discipline throughout is genuinely strong — the gate is narrow and local.
Implements Phase P2 of ADR-021 short of the live smoke run: the agent is now fully launchable and observable on the
lambda-microvmbackend. Follows #689 (P1). Honest scope note: the clone→change→PR smoke run itself (plus the deferred empirical items: suspend TTL >1h, SUSPENDED-vs-quota,microvmImageHooksAPI spelling,NO_INGRESSARN) executes against a live account after this lands and is tracked on #645 — this PR is the foundations, not the completion claim.Platform config travels with the task, not the image
MicroVM env vars are image-version-frozen, and external images can't receive stack env at all — so deployment identifiers (table names, secret ARNs, session-role ARN; 13-key allowlist, 4 required) now ride the
/runenvelope as aplatform_configblock. The agent installs only allowlisted keys into its environment before any credential/pipeline initialization and fails closed on unknown keys (env installation from a network payload = injection surface). The allowlist lives once incontracts/constants.json; both the agent and the CDK strategy derive from it andcheck-constants-syncforbids literal re-declarations. Canonical wire shapes are documented in ADR-021 §3.Snapshot credential hygiene (real defect found and fixed)
/readywas spinning up the CloudWatch debug writer, which resolved a credential chain and pinned the build-time region intoboto3.DEFAULT_SESSION— state the image snapshot would replay into every MicroVM. Build hooks and pre-install/runlogging are now stdout-only; a poisoned-seam test suite (every AWS/credential entry point armed to throw) plus a subprocess regression test lock the property, mutation-checked.Full hook set + IAM parity + dual-signal liveness
The image now declares exactly what the agent serves:
ready+validate(image hooks;/validatemakes zero AWS calls — it runs under the build role) andrun+terminate(runtime;/terminatereturns 200 for any body and never writes terminal task status — the orchestrator owns terminal state). The execution role gains feature-based runtime parity (GitHub PAT + channel-OAuth secrets, scoped Bedrock, Memory grantReadWrite, AZ describe — still no direct DynamoDB; tenant data flows through the SessionRole). Heartbeat-staleness liveness now coverslambda-microvm(exhaustive per-backend switch; agentcore byte-identical, ECS excluded, RUNNING-scoped so P3 suspends stay immune) — closing the hung-pipeline-behind-healthy-substrate blind spot.Docs
COMPUTE.md gains the Lambda MicroVMs column + explicit classic-Lambda distinction (the #645 acceptance criterion) with live-verified values only; ORCHESTRATOR.md gains the dual-signal liveness + lifecycle sections; SECURITY.md covers all three compute roles; ADR-021 amended in place (
proposed).Verification: cdk 3 825 / agent 1 590 (82.8% cov) / cli untouched; tsc, eslint, ruff/ty/vulture clean; drift-prevention green; docs sync idempotent + astro check clean; gitleaks clean.
security:sastruns in CI (semgrep unavailable locally).Refs #645