Skip to content

Commit f3cfb4e

Browse files
dreamorosiclaudekrokoko
authored
feat(compute): add AWS Lambda MicroVMs ComputeStrategy backend — P1 (#645) (#689)
* feat(compute): add AWS Lambda MicroVMs ComputeStrategy backend — P1 (#645) Implement Phase P1 of ADR-021: the lambda-microvm compute backend (start/poll/stop), its CDK infrastructure, bootstrap policy, CLI support, and regional-availability enforcement. Suspend/resume (P3) and agent-side hook serving / smoke parity (P2) follow separately; a P1-built image is documented as not yet runnable end to end. Handlers (cdk/src/handlers): - ComputeType widens to 'agentcore' | 'ecs' | 'lambda-microvm'; SessionHandle gains {microvmId, endpoint}; SessionStatus gains 'suspended'; resolveComputeStrategy keeps the exhaustive-never gate - LambdaMicrovmComputeStrategy: RunMicrovm with idlePolicy omitted (auto-suspend disabled by construction) and maximumDurationInSeconds=28800 (AgentCore 8h parity); payload inline in runHookPayload up to exactly 16384 bytes, S3 pointer above; GetMicrovm poll maps the six-state MicrovmState enum mechanically (no task-state interpretation in the strategy); TerminateMicrovm best-effort with differentiated error handling; errors wrapped with a MicroVM marker so classifier entries cannot leak retry semantics onto other backends (regression-pinned) - Orchestrator: persists {microvmId, endpoint} to compute_metadata; cross-references substrate state vs DynamoDB (terminal + non- terminal -> failed with confirm re-read; suspended + non- AWAITING_APPROVAL -> anomaly event, no fail-fast); finalization and cancellation terminate the MicroVM (cancel branch ordered before the AgentCore RUNTIME_ARN fallback to avoid stopping an unrelated runtime in mixed deployments) Infra (cdk/src/constructs, stacks, bootstrap): - LambdaMicrovmCompute construct: CfnMicrovmImage + CfnNetworkConnector L1s (platform-VPC egress, 443-only SG), build/ execution roles trusted by lambda.amazonaws.com with sts:TagSession + aws:SourceAccount, execution role admitted via AgentSessionRole.admitComputeRole, payload bucket (1-day expiry, execution-role read-only, orchestrator put-only), three documented image-config states with mutually exclusive synth warnings - IAM scoped to the exact image ARN (+ ':*' version hedge; image names cannot contain ':') — no microvm-image:* wildcard anywhere; orchestrator gets only RunMicrovm/GetMicrovm/TerminateMicrovm/ PassNetworkConnector + scoped iam:PassRole; cancel Lambda gets TerminateMicrovm only; no Suspend/Resume/auth-token grants (P3) - Synth-time region gate (5 launch regions) with microvm_region_override escape hatch; token regions skipped - Bootstrap: conditional IaCRole-ABCA-Compute-LambdaMicrovms policy behind ComputeTypes (1.2.0 -> 1.3.0), deploy-time actions only; DEPLOYMENT_ROLES.md golden block [5] + golden-baseline parity test - abca:compute-backend=lambda-microvm cost tags; scripts/package-microvm-artifact.sh for the zip+Dockerfile flow CLI (cli/src): - Onboarding probes availability (ListManagedMicrovmImages) on the EFFECTIVE compute type (flag ?? stored ?? default) before any write, rejecting with launch-region list + agentcore/ecs remedy - platform doctor check when an active blueprint uses the backend (access-denied -> warn per checkBedrockModel precedent); runtime status groups the substrate like ECS ADR-021 refined in place (proposed): six-state poll mapping table, GetMicrovm NotFound -> completed rationale, microvmId vs microvmIdentifier seam, per-hook phasing table and the explicit "P1 image is not runnable end to end" consequence. Verification: cdk 141 suites / 2744 tests; cli 52 suites / 651 tests; tsc + eslint clean in both; types-sync green; knip at baseline (78); bootstrap generation and docs sync deterministic. Refs #645 Co-authored-by: Claude <noreply@anthropic.com> * fix(deps): regenerate yarn.lock with CI toolchain honoring root resolutions The lock regenerated while adding @aws-sdk/client-lambda-microvms did not honor the root resolutions field, reintroducing vulnerable brace-expansion pins (GHSA-mh99-v99m-4gvg, cleared on main in #658) and drifting from the resolution shape CI's install produces (yaml dedupe to 2.9.0, strnum/xml-naming entries), which tripped the fail-on-mutation gate. Regenerated with Node 22.23.2 + Yarn 1.22.22 (CI-equivalent) via yarn install --check-files: brace-expansion collapses to patched 5.0.9 everywhere, osv-scanner 2.4.0 reports no issues, second install is byte-identical, and both workspaces stay green (cdk 2744, cli 651; tsc clean). Refs #645 Co-authored-by: Claude <noreply@anthropic.com> * test: cover remaining patch lines flagged by codecov Close the 14 uncovered patch lines on PR #689: platform-doctor repo lookup failure shapes (non-Error throw, missing table output, lookup error, empty active-repo list) and non-Error MicroVM probe formatting; orchestrate-task reconciliation task-failure path; and the agent stack's defensive missing-image-ARN invariant. Tests only, no production changes; no unreachable lines found. cli 655, cdk 2746, tsc + eslint clean in both. Refs #645 Co-authored-by: Claude <noreply@anthropic.com> * fix(scripts): use service API shapes in microvm packaging script The create-microvm-image call must match the Lambda MicroVMs API model (the AWS CLI is generated from it): architecture is ARM_64 and hooks are enable flags (run: ENABLED) with service-defined paths - not the CFN L1's string shapes the script had copied. The construct is intentionally unchanged: CFN fields are unconstrained strings with their own spec conventions, validated at deploy time. Caught while drafting the P1 verification runbook; offline tests cannot exercise API payload shapes. bash -n clean; cdk suite green. Refs #645 Co-authored-by: Claude <noreply@anthropic.com> * fix(compute): correct P1 against live-service behavior + review items (#645) A live verification run (us-east-1) proved five constants/assumptions wrong against the real Lambda MicroVMs service; a provenance review traced each to its source; PR #689 review requested an onboard gate. This lands all corrections: Live-service fixes: - RunMicrovm requires an image ARN: construct injects the derived ARN as MICROVM_IMAGE_IDENTIFIER; strategy validates via assertImageArn before the payload upload - runHookPayload cap is 4096 bytes (docs/SDK prose say 16384 - AWS docs tickets filed): boundary tests at 4096/4097; S3 pointer is now the dominant path - VPC_EGRESS connectors require an operator role (live 400 refuted the service-linked-role comment): shared operator role on both connectors, probe-validated policy shape - Ingress defaults to a PUBLIC HTTP_INGRESS connector: NO_INGRESS is now an explicit, all-or-nothing control (required construct prop, unconditionally injected env, region-derived strategy fallback); tests assert the outcome, not field omission - minimumMemoryInMiB is a BASELINE capped at 8192 MiB with automatic 4x vertical scaling to a 32 GiB peak - validated prop + reframed ADR/docs (the 32 GB launch figure is the peak, not the input) - Agent now serves /aws/lambda-microvms/runtime/v1/ready and /run (the service refuses lifecycle hooks without /ready; a hook-less image cannot accept runHookPayload): /run reuses the /invocations background-spawn path; hooks re-phased ready+run into P1 - Image builds need port 80 (apt): separate build-time egress connector + SG; runtime stays 443-only - Script: ARM_64/ENABLED shapes, banner before+after create, ERR trap Review items (PR #689): - Onboard gates on the stack's ComputeSubstrate output for lambda-microvm (ECS parity) via a shared comma-membership helper, ordered before the availability probe - microvm_suspend_anomaly emits once per episode (PollState flag, re-arms on recovery, carries through poll failures) - Orphan MicroVM reaped best-effort when startSession succeeds but persistence fails (both windows: orchestrator catch and strategy missing-endpoint), never masking the original error - Incomplete RunMicrovm response wrapped with the MicroVM classifier marker ADR-021 amended in place (proposed): 4 KB payload, baseline/peak memory, explicit-ingress security row, hook re-phasing, TERMINATED as the load-bearing terminal signal (NotFound is late), source-hierarchy note for externally-sourced service facts. Mirror regenerated. Verification: cdk 2796, cli 683, agent pytest 1322; tsc/eslint/ruff clean; docs sync idempotent. Refs #645 Co-authored-by: Claude <noreply@anthropic.com> * fix(deps): clear 25 osv-scanner advisories across three lockfiles Ecosystem advisories against pre-existing pins (main's weekly security scan fails identically), gating this PR: - agent/uv.lock: cryptography 49.0.0 -> 50.0.0 (GHSA-g6cj-pr64-35w5, High; transitive via mcp[crypto] -> pyjwt), minimal uv lock --upgrade-package - integrations/jira-forge-app: fast-uri 3.1.5, undici 7.29.0 (transitive via @forge/manifest) - yarn.lock: 9 further advisories cleared via existing-style root resolutions for fast-uri, ip-address, undici; lock regenerated with the CI toolchain (Node 22.23.2 + Yarn 1.22.22) and verified byte-stable across repeated installs osv-scanner: no issues found. Suites re-verified after resolution changes: cdk 3736, cli 722, agent 1460, jira-forge 9; tsc clean. Refs #645 Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alain Krok <alkrok@amazon.com>
1 parent d3974f7 commit f3cfb4e

75 files changed

Lines changed: 9448 additions & 212 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,34 @@ Immediate response (acceptance):
218218

219219
Final metrics (PR URL, cost, turns, build status, etc.) appear in **container logs**, in **DynamoDB** when configured, and in the **REST API** for deployed tasks (`GET /v1/tasks/{task_id}` via the `bgagent` CLI or HTTP client).
220220

221+
### AWS Lambda MicroVMs lifecycle hooks (ADR-021 P1)
222+
223+
The same uvicorn process also serves the **Lambda MicroVMs** lifecycle hooks, on the same port (8080 — the port declared in the image's `hooks.port`). On that backend there is no `InvokeAgentRuntime` and no orchestrator→agent HTTP path at all: the task payload arrives as the `/run` hook body and nothing else dials in.
224+
225+
**`POST /aws/lambda-microvms/runtime/v1/ready`** — Build hook. Returns `{"status": "ready"}` as soon as the server is up, which is the signal the service waits for before taking the snapshot. **Mandatory**, not optional: `CreateMicrovmImage` refuses an image that enables *any* lifecycle hook without `/ready`, and with the hook enabled but unserved every build fails with `Ready hook check failed: the application returned a client error (HTTP 4xx) response`.
226+
227+
**`POST /aws/lambda-microvms/runtime/v1/run`** — Payload delivery. Validates the body, starts the pipeline in a background thread (the same `_extract_invocation_params``_spawn_background` path `/invocations` uses), and returns 200 inside the 1–60 s hook budget. Body:
228+
229+
```json
230+
{
231+
"microvmId": "microvm-b44b69d9-…",
232+
"runHookPayload": "{\"agent_payload_s3_uri\": \"s3://bucket/<task_id>/payload.json\"}"
233+
}
234+
```
235+
236+
`runHookPayload` is an opaque **string** the service passes through from `RunMicrovm`. ABCA's contract for it is one of two shapes, mirroring the ECS container env contract (`AGENT_PAYLOAD` / `AGENT_PAYLOAD_S3_URI`):
237+
238+
| Envelope | When |
239+
|---|---|
240+
| `{"agent_payload": {…}}` | the whole orchestrator payload inline — only when it fits |
241+
| `{"agent_payload_s3_uri": "s3://bucket/key"}` | pointer to the payload in the platform payload bucket |
242+
243+
The service caps `runHookPayload` at **4 096 bytes**, so the **pointer form is the normal one** — a hydrated payload is essentially always larger. Fetching it needs no new env var: the MicroVM execution role holds read-only access to that bucket and the URI carries bucket + key.
244+
245+
Rejections are structured so they are readable in the MicroVM log group: `400 MICROVM_RUN_PAYLOAD_INVALID` (unusable envelope — retrying the same body cannot help), `500 MICROVM_RUN_PAYLOAD_UNREADABLE` (the S3 fetch failed), `400 TASK_RECORD_INCOMPLETE` (same validator and vocabulary as `/invocations`).
246+
247+
`/validate` (build) and `/suspend`, `/resume`, `/terminate` (runtime) are deliberately **not** served — declaring a hook nothing answers fails the corresponding build or lifecycle transition, so the CDK construct declares exactly `/ready` + `/run`. `/terminate` and `/validate` land in P2; `/suspend` + `/resume` in P3 with the ComputeStrategy interface widening.
248+
221249
### Testing Server Mode Locally
222250

223251
Use `run.sh --server` to build and start the server locally. It handles credentials, port mapping, and resource constraints automatically:
@@ -375,7 +403,7 @@ agent/
375403
│ ├── repo.py Repository setup: clone, branch, git auth, mise trust/install/build/lint
376404
│ ├── shell.py Shell utilities: log(), run_cmd(), redact_secrets(), slugify(), truncate()
377405
│ ├── telemetry.py Metrics, disk usage, trajectory writer (_TrajectoryWriter with write_policy_decision)
378-
│ ├── server.py FastAPI — async /invocations (background thread), /ping health check, heartbeat daemon; OTEL session correlation
406+
│ ├── server.py FastAPI — async /invocations (background thread), /ping health check, MicroVM /ready + /run lifecycle hooks, heartbeat daemon; OTEL session correlation
379407
│ ├── task_state.py Best-effort DynamoDB task status and heartbeat writes (no-op if TASK_TABLE_NAME unset)
380408
│ ├── observability.py OpenTelemetry helpers (e.g. AgentCore session id)
381409
│ ├── memory.py Optional memory / episode integration for the agent

agent/src/server.py

Lines changed: 231 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
"""FastAPI server for AgentCore Runtime.
1+
"""FastAPI server for AgentCore Runtime and Lambda MicroVMs.
22
3-
Exposes /invocations (POST) and /ping (GET) on port 8080,
4-
matching the AgentCore Runtime container contract.
3+
Exposes /invocations (POST) and /ping (GET) on port 8080, matching the AgentCore
4+
Runtime container contract, plus the AWS Lambda MicroVMs lifecycle hooks under
5+
``/aws/lambda-microvms/runtime/v1/`` (ADR-021 P1) on the same port.
56
6-
The /invocations handler accepts the task, spawns a background thread to run
7-
the pipeline, and returns a small JSON acceptance immediately. Task progress
8-
is tracked in DynamoDB via ``task_state`` + ``ProgressWriter``.
7+
Both entry paths accept the task, spawn a background thread to run the pipeline,
8+
and return a small JSON acceptance immediately. Task progress is tracked in
9+
DynamoDB via ``task_state`` + ``ProgressWriter``.
910
"""
1011

1112
import asyncio
1213
import contextlib as _ctx_for_debug
14+
import json
1315
import logging
1416
import os
1517
import threading
@@ -814,3 +816,226 @@ async def invoke_agent(request: Request, body: InvocationRequest):
814816
}
815817
}
816818
)
819+
820+
821+
# --------------------------------------------------------------------------
822+
# AWS Lambda MicroVMs lifecycle hooks (ADR-021 P1)
823+
# --------------------------------------------------------------------------
824+
# The MicroVM backend has NO orchestrator→agent HTTP path: the task payload
825+
# arrives as the ``/run`` hook's request body and nothing else dials in. The
826+
# service calls these routes on the port declared in the image's ``hooks.port``
827+
# (8080 — the same uvicorn process that serves /invocations and /ping), so the
828+
# hooks live here rather than in a sidecar.
829+
#
830+
# Only ``/ready`` and ``/run`` are implemented, and both are P1:
831+
# * ``/ready`` is MANDATORY. ``CreateMicrovmImage`` refuses an image that
832+
# enables ANY lifecycle hook without it ("The ready (/ready) MicroVM image
833+
# hook must be enabled when any MicroVM lifecycle hook … is enabled"), and an
834+
# image with no hooks at all cannot receive a ``runHookPayload``. So ADR-021's
835+
# original "declare /run in P1, serve it in P2" split was not a reachable
836+
# service state.
837+
# * ``/run`` is the payload-delivery channel.
838+
# ``/suspend`` and ``/resume`` are P3 (they need the ComputeStrategy interface
839+
# widening), and ``/validate`` + ``/terminate`` are P2 polish. Declaring a hook
840+
# the agent does not answer fails the corresponding build or lifecycle
841+
# transition, which is why the construct declares exactly these two.
842+
MICROVM_HOOK_PREFIX = "/aws/lambda-microvms/runtime/v1"
843+
844+
#: ``s3://`` scheme prefix for the out-of-band payload pointer.
845+
_S3_URI_SCHEME = "s3://"
846+
847+
848+
class MicrovmRunHookRequest(BaseModel):
849+
"""Body the MicroVM service POSTs to the ``/run`` hook.
850+
851+
``runHookPayload`` is the opaque STRING the orchestrator passed to
852+
``RunMicrovm`` — the service does not parse it. ABCA's contract for that
853+
string (``lambda-microvm-strategy.ts``) is one of two shapes, mirroring the
854+
ECS container env contract (``AGENT_PAYLOAD`` / ``AGENT_PAYLOAD_S3_URI``):
855+
856+
* ``{"agent_payload": {...}}`` — the whole orchestrator payload, inline.
857+
* ``{"agent_payload_s3_uri": "s3://bucket/key"}`` — a pointer to it.
858+
859+
The pointer form is the DOMINANT one: the service caps ``runHookPayload`` at
860+
4 096 bytes and a hydrated payload is essentially always larger.
861+
862+
Both fields default to empty so a malformed call produces this module's
863+
structured 400 rather than FastAPI's 422 — the service surfaces a 4xx as a
864+
generic "client error" hook failure either way, and our own body is what ends
865+
up in the MicroVM log group.
866+
"""
867+
868+
microvmId: str = "" # service field name; camelCase on the wire
869+
runHookPayload: str = "" # service field name; camelCase on the wire
870+
871+
872+
def _fetch_microvm_payload_from_s3(uri: str) -> dict:
873+
"""Read and parse the out-of-band ``/run`` payload from S3.
874+
875+
Same fetch the ECS boot command performs for ``AGENT_PAYLOAD_S3_URI``; the
876+
MicroVM **execution role** holds the read grant, scoped to the platform
877+
payload bucket. Errors propagate to the caller, which turns them into a
878+
structured 400/500 — silently starting a pipeline with no payload would
879+
produce a task that runs with an empty prompt.
880+
"""
881+
remainder = uri[len(_S3_URI_SCHEME) :]
882+
bucket, _, key = remainder.partition("/")
883+
if not bucket or not key:
884+
raise ValueError(f"agent_payload_s3_uri is not a bucket/key URI: {uri!r}")
885+
886+
import boto3
887+
888+
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
889+
client = boto3.client("s3", region_name=region)
890+
body = client.get_object(Bucket=bucket, Key=key)["Body"].read()
891+
payload = json.loads(body)
892+
if not isinstance(payload, dict):
893+
raise ValueError(f"S3 payload at {uri!r} is {type(payload).__name__}, expected an object")
894+
return payload
895+
896+
897+
def _resolve_microvm_run_payload(run_hook_payload: str) -> dict:
898+
"""Turn the ``runHookPayload`` string into the orchestrator payload dict.
899+
900+
Raises ``ValueError`` for every shape the agent cannot act on, so the caller
901+
has exactly one failure branch to map onto a 400.
902+
"""
903+
if not run_hook_payload.strip():
904+
raise ValueError("runHookPayload is empty")
905+
906+
try:
907+
envelope = json.loads(run_hook_payload)
908+
except json.JSONDecodeError as exc:
909+
raise ValueError(f"runHookPayload is not valid JSON: {exc}") from exc
910+
911+
if not isinstance(envelope, dict):
912+
raise ValueError(f"runHookPayload must be a JSON object, got {type(envelope).__name__}")
913+
914+
inline = envelope.get("agent_payload")
915+
if inline is not None:
916+
if not isinstance(inline, dict):
917+
raise ValueError(f"agent_payload must be an object, got {type(inline).__name__}")
918+
return inline
919+
920+
uri = envelope.get("agent_payload_s3_uri")
921+
if isinstance(uri, str) and uri.startswith(_S3_URI_SCHEME):
922+
return _fetch_microvm_payload_from_s3(uri)
923+
if uri is not None:
924+
raise ValueError(f"agent_payload_s3_uri must be an s3:// URI, got {uri!r}")
925+
926+
raise ValueError(
927+
"runHookPayload envelope has neither agent_payload nor agent_payload_s3_uri "
928+
f"(keys: {sorted(envelope)})"
929+
)
930+
931+
932+
@app.post(f"{MICROVM_HOOK_PREFIX}/ready")
933+
def microvm_ready():
934+
"""MicroVM image ``/ready`` build hook — "the application has initialised".
935+
936+
A 200 from this route is the signal the service waits for before taking the
937+
snapshot, so answering it at all is what makes the image buildable: with the
938+
hook enabled and nothing serving it, both chipset builds fail with "Ready hook
939+
check failed: the application returned a client error (HTTP 4xx) response".
940+
941+
Reaching this handler already proves everything P1 needs: uvicorn is bound on
942+
the hook port and ``server`` imported cleanly (which pulls in ``pipeline`` →
943+
``runner`` → the policy engine, so a missing policy file or a broken import
944+
fails the BUILD instead of the first task). Deeper warm-up assertions —
945+
Bedrock reachability, Memory access, tool availability — belong to P2's
946+
``/validate``, which is deliberately still not declared: a hook that 404s or
947+
reports failure fails every image build.
948+
949+
Declared ``def`` rather than ``async def`` on purpose: Starlette runs sync
950+
handlers in a threadpool, so this never competes with the event loop that has
951+
to keep ``GET /ping`` fast.
952+
"""
953+
_debug_cw("/ready hook: server is up, reporting ready for snapshot")
954+
return {"status": "ready"}
955+
956+
957+
@app.post(f"{MICROVM_HOOK_PREFIX}/run")
958+
def microvm_run(request: Request, body: MicrovmRunHookRequest):
959+
"""MicroVM ``/run`` lifecycle hook — accept the task and start the pipeline.
960+
961+
Fast-notification contract (1-60 s hook budget): validate, spawn, return 200.
962+
The pipeline itself must NOT run on the hook path, so this reuses the exact
963+
mechanism ``/invocations`` uses — ``_extract_invocation_params`` →
964+
``_validate_required_params`` → ``_spawn_background`` — rather than a second,
965+
drifting payload mapper. The orchestrator payload is byte-identical across
966+
substrates (AgentCore receives it as ``input``, ECS as ``AGENT_PAYLOAD``,
967+
MicroVMs inside this envelope), which is what makes that reuse correct.
968+
969+
Session/workload headers are absent here (there is no AgentCore Runtime in
970+
front of this call), so ``_extract_invocation_params`` resolves an empty
971+
``session_id`` / workload token — the same posture the ECS backend already
972+
has, per ADR-021 sub-decision 3's identity delta.
973+
974+
Sync ``def`` for the same reason as ``/ready``, and additionally because the
975+
S3 payload fetch is a blocking boto3 call: in a threadpool it cannot stall
976+
the event loop.
977+
"""
978+
_debug_cw(f"/run hook received: microvm_id={body.microvmId!r} bytes={len(body.runHookPayload)}")
979+
980+
try:
981+
payload = _resolve_microvm_run_payload(body.runHookPayload)
982+
except ValueError as exc:
983+
# Bad envelope — the orchestrator built something this agent cannot act
984+
# on. 400 (not 500) because retrying an identical body cannot help.
985+
_warn_cw(f"/run hook rejected: {exc}")
986+
return JSONResponse(
987+
status_code=400,
988+
content={
989+
"code": "MICROVM_RUN_PAYLOAD_INVALID",
990+
"message": str(exc),
991+
},
992+
)
993+
except Exception as exc:
994+
# Payload fetch failed (S3 AccessDenied / NoSuchKey / transient). 500 so
995+
# the failure is distinguishable from a malformed body, and loud enough to
996+
# find in the MicroVM log group.
997+
_debug_cw_exc("/run hook payload fetch FAILED", exc)
998+
return JSONResponse(
999+
status_code=500,
1000+
content={
1001+
"code": "MICROVM_RUN_PAYLOAD_UNREADABLE",
1002+
"message": f"{type(exc).__name__}: {exc}",
1003+
},
1004+
)
1005+
1006+
task_id_log = str(payload.get("task_id", ""))
1007+
try:
1008+
params = _extract_invocation_params(payload, request)
1009+
except Exception as exc:
1010+
_debug_cw_exc(
1011+
"/run hook _extract_invocation_params FAILED", exc, task_id=task_id_log or None
1012+
)
1013+
raise
1014+
1015+
missing = _validate_required_params(params)
1016+
if missing:
1017+
_debug_cw(
1018+
f"/run hook rejected: missing required params {missing!r}",
1019+
task_id=task_id_log or None,
1020+
)
1021+
return JSONResponse(
1022+
status_code=400,
1023+
content={
1024+
"code": "TASK_RECORD_INCOMPLETE",
1025+
"message": (
1026+
"Task record is missing required fields. The orchestrator "
1027+
"should have populated these before starting the MicroVM."
1028+
),
1029+
"missing": missing,
1030+
},
1031+
)
1032+
1033+
_spawn_background(params)
1034+
task_id = params["task_id"]
1035+
_debug_cw(f"/run hook accepted task_id={task_id!r}", task_id=task_id or None)
1036+
return {
1037+
"status": "accepted",
1038+
"task_id": task_id,
1039+
"microvm_id": body.microvmId,
1040+
"timestamp": datetime.now(UTC).isoformat(),
1041+
}

0 commit comments

Comments
 (0)