- This file is the living handoff document for the migration.
- Goal: migrate the current CLI to a Lambda-backed architecture in a sequence of separate PRs, where each PR ends with a locally invokable, fully working command path against deployed AWS.
- Initial migration order:
status->terminate->launch->new->delete-project-> full cleanup/cutover. - CLI rollout model: hybrid by command. Migrated commands go remote; unmigrated commands keep current local behavior until their phase lands.
- Transport: Lambda Function URL from day one.
- Discovery: CLI resolves the Function URL from SSM at
${param_prefix}/cli/functionUrl. - Auth: CLI signs HTTP requests with AWS SigV4.
- Response model: NDJSON event stream with
progress,warning,result,success, anderror. - Python streaming implementation: Lambda Web Adapter.
- Packaging: the original shared-image plan is superseded for phase 1. The CLI Lambda gets its own image because Lambda Web Adapter would interfere with the existing handler-based Lambdas if we baked it into the current shared image.
- Protocol ownership: shared wire-format constants plus the action and event enums live in
src/devbox/cli_protocol.py. Future actions should extend that shared module instead of re-declaring protocol strings in the CLI and Lambda separately. - Lambda routing: the CLI Lambda router uses an explicit dispatch table keyed by the shared action enum. Future actions should add themselves to that dispatch table rather than branching on ad hoc string comparisons.
- Command-module layout: command-specific client-side and Lambda-side behavior now lives under
src/devbox/commands/<action>.py.cli.pyremains the Click entry point,remote_client.pyremains generic transport, andcli_lambda/app.pyremains the generic dispatch layer. - Python documentation style: new Python code for this migration should use numpydoc-style docstrings for modules, public functions, and non-trivial helpers. Exception: Click entrypoints should keep user-facing docstrings, because Click surfaces those docstrings in
--helpoutput and they are part of the CLI UX rather than internal developer documentation. - IAM layout: keep one CLI Lambda IAM statement per action wherever practical, and make each action block self-contained so its required permissions can be reviewed in isolation even when that duplicates shared read permissions.
- Phase 1
statusLambda permissions are intentionally EC2-only. The local CLI resolves the Function URL from SSM, and the Lambda-sidestatushandler currently reusesDevBoxManager.list_*inventory helpers that call EC2Describe*APIs only. - Documentation authority: this file is authoritative for phase 1. Any older standalone CLI migration spec is advisory only until it is reconciled back into this document.
- Dependencies:
requestsmay be added to runtime dependencies;responsesmay be added to test dependencies. - End-of-phase validation: real local CLI invocation against deployed AWS, not just local mocks or container smoke tests.
newis deferred but remains in scope, even though the current CLI references a missing implementation.
- CLI request envelope:
{
"version": "v1",
"action": "status",
"request_id": "uuid",
"param_prefix": "/devbox",
"payload": {
"project": null
}
}- Lambda event envelope:
{
"type": "progress|warning|result|success|error",
"action": "status",
"message": "human-readable text",
"data": {}
}- The CLI remains responsible for:
- resolving the Function URL from SSM
- signing requests
- reading local files such as
--userdata-file - rendering streamed output
- keeping local confirmation prompts for destructive flows
- The Lambda remains responsible for:
- validating the request envelope
- performing AWS-side operations
- emitting versioned event streams
- enforcing server-side safety checks
Milestone: devbox status [project] runs from the local CLI through the deployed CLI Lambda and renders the same tables as today.
- Create the authoritative wire-contract section in this file for
status, including request payload, result payload, and terminal event rules. - Add runtime support for HTTP invocation in the CLI, using
requestsplus SigV4 signing helpers. - Add a shared remote-invocation layer in the CLI for:
- SSM Function URL lookup
- request envelope creation
- signed HTTP POST
- NDJSON event parsing
- terminal-event validation
- error mapping to current CLI exit behavior
- Add a new CLI Lambda handler/router in a separate CLI Lambda image.
- Add Lambda Web Adapter support to the CLI Lambda image build.
- Add Terraform resources for the CLI Lambda, Function URL, IAM, log group, and SSM parameter publication.
- Implement the remote
statusaction in Lambda by reusing the existingDevBoxManagerinventory logic rather than duplicating AWS queries. - Define and implement timestamp serialization in Lambda and timestamp rehydration in the CLI so existing console rendering still works.
- Keep
statusas the only migrated command in this phase; all other commands remain local. - Add tests for:
- Function URL lookup
- signed request dispatch
- streamed NDJSON parsing
- successful
statusresult rendering - malformed stream handling
- HTTP/auth failure handling
- missing SSM parameter handling
- request envelope validation
- event encoding
- action dispatch and failure mapping
- Run
pixi run -e dev python -m pytestfor touched tests. - Run
tofu fmtfor touched Terraform. - Run
tofu validatefor touched Terraform. - Record the exact local end-to-end validation command and observed outcome in this file.
- Request envelope:
{
"version": "v1",
"action": "status",
"request_id": "uuid",
"param_prefix": "/devbox",
"payload": {
"project": null
}
}payload.projectmay be a project name string ornull.- Success path event sequence:
- exactly one
resultevent with the full status payload - exactly one terminal
successevent
- exactly one
statusresult payload shape:
{
"instances": [
{
"InstanceId": "i-0123456789abcdef0",
"Project": "my-project",
"PublicIpAddress": "54.0.0.1",
"LaunchTime": "2026-03-30T20:30:00+00:00",
"State": "running",
"InstanceType": "t3.medium"
}
],
"volumes": [
{
"VolumeId": "vol-0123456789abcdef0",
"Project": "my-project",
"State": "in-use",
"Size": 100,
"AvailabilityZone": "us-east-1a",
"IsOrphaned": false
}
],
"snapshots": [
{
"SnapshotId": "snap-0123456789abcdef0",
"Project": "my-project",
"Progress": "100%",
"VolumeSize": 100,
"StartTime": "2026-03-30T18:00:00+00:00",
"IsOrphaned": false
}
]
}- The CLI must rehydrate
LaunchTimeandStartTimetodatetimeobjects before callingConsoleOutput. - Phase 1
statusdoes not render progress events; warnings and errors may still be surfaced immediately.
2026-03-31: targeted phase 1 pytest suite passed after the command-module refactor.- Command:
pixi run -e dev python -m pytest tests/commands/test_status.py tests/cli_lambda/test_contracts.py tests/cli_lambda/test_app.py tests/test_remote_client.py tests/test_cli.py -q - Result:
84 passed, 1 warning
- Command:
2026-03-31: Terraform formatting passed.- Command:
tofu fmt - Result: passed
- Command:
2026-03-31: Terraform validation passed in the real deployment environment after the fresh-account/Public ECR auth fixes were applied.- Command:
tofu validate - Result: passed
- Command:
- End-to-end commands (must be run by DWHS in devbox-deploy/ repo, not for agents)
tofu plan -out tfplan && tofu apply tfplanto deploy the CLI Lambda and related infradevbox statusagainst the deployed CLI Lambda, expecting to see the desired output
- Observed outcome: It works!
- The shared wire contract now lives in
src/devbox/cli_protocol.py. ExtendCliActionand the shared protocol constants there first whenever a new remote command is added. tests/cli_lambda/test_contracts.pyowns low-level request/event contract coverage.tests/cli_lambda/test_app.pyowns dispatch and execution-failure behavior. Keep that split as more actions land.tests/commands/test_status.pynow owns thestatuscommand semantics across both CLI-side and Lambda-side behavior. Keep command-specific tests near the command module instead of spreading them across transport and app test files.- The CLI Lambda router in
src/devbox/cli_lambda/app.pyis organized aroundACTION_HANDLERS. Future actions should wire themselves into that table and add parametrized coverage rather than growing one-off branching tests. statusis the template command for the newsrc/devbox/commands/<action>.pylayout. Future simple commands should follow that module pattern first; only extract a shared helper after a second migrated command proves the duplication is real.- The CLI Lambda image now uses
python:3.13-slimdirectly instead of the AWS public mirrorpublic.ecr.aws/docker/library/python:3.13-slim, because the mirrored image returned403 Forbiddenduringdocker buildin a fresh account deployment. - The CLI Lambda image now uses Lambda Web Adapter
1.0.0, matching the current official README example for container images. - The CLI Lambda
local-execbuild step now logs into Public ECR explicitly withaws ecr-public get-login-passwordbeforedocker build, because the adapter image pull failed with expired or missing Public ECR auth in a fresh-account deployment. - The CLI Lambda
local-execbuild step now uses a temporaryDOCKER_CONFIGso Terraform does not depend on or mutate the operator's persistent Docker Desktop keychain state on macOS. - The phase 1 IAM policy in
modules/cli-lambda/main.tfis intentionally grouped per action. Keep that structure, and prefer each action block to list the full permission set that command needs in isolation even when that duplicates shared EC2 read permissions. - Do not add SSM or DynamoDB permissions to the CLI Lambda just because
DevBoxManagercan use them elsewhere. Add them only when a migrated Lambda action actually reads those services. - The request envelope still carries
param_prefixfrom the client. That is acceptable in phase 1 becausestatusonly reuses EC2Describe*inventory helpers and the CLI Lambda IAM is EC2-only, so the prefix does not currently select SSM or DynamoDB resources. Before any later action uses prefix-derived SSM parameter names, DynamoDB table names, or other non-EC2 resources, add server-side validation/normalization for the prefix or replace the client-provided value with Lambda-side configuration. src/devbox/remote_client.pynow wrapsrequeststransport failures asRemoteInvocationError, but it still uses a singletimeout=30value. Revisit timeout policy in phase 3 before migratinglaunch, because long-lived streaming commands may need separate connect/read timeouts or a longer read timeout to avoid aborting a healthy response stream mid-operation.- Remaining phase 1 blockers: none. Phase 1 and the inter-phase command-module refactor are complete.
- Next session starts here: begin phase 2
terminateusing thesrc/devbox/commands/<action>.pypattern established bystatus, and preserve the current remote contract/transport structure.
Milestone: status command-specific behavior is co-located under src/devbox/commands/status.py, while cli.py, remote_client.py, and cli_lambda/app.py remain generic entrypoint, transport, and router layers.
- Update this file to record the command-module layout decision and handoff guidance.
- Add
src/devbox/commandsand package it for distribution. - Move
statusclient-side and Lambda-side command behavior intosrc/devbox/commands/status.py. - Keep
src/devbox/remote_client.pygeneric by removingstatus-specific helpers. - Keep
src/devbox/cli.pyas a thin Click wrapper that delegates to thestatuscommand module. - Keep
src/devbox/cli_lambda/app.pyas the generic router and dispatchstatusthrough the command module. - Move
statuscommand tests totests/commands/test_status.py. - Run the targeted refactor validation suite and record the result here.
Milestone: devbox terminate <instance-id-or-project> runs from the local CLI through the deployed CLI Lambda and preserves current success and failure behavior.
- Extend the wire contract for
terminate, including request payload and result payload. - Add the remote
terminateaction to the Lambda router. - Reuse the existing termination logic behind a Lambda-safe interface rather than reimplementing termination rules in the HTTP layer.
- Expand CLI Lambda IAM only with the permissions required for termination.
- Migrate only
terminatein the CLI to the remote path. - Preserve current CLI syntax and current visible success/error messages as closely as practical.
- Add tests for:
- terminate by instance ID
- terminate by project name
- not-found behavior
- multiple-instance ambiguity behavior
- terminal
errorevent mapping - transport failure behavior
- Run
pixi run -e dev python -m pytestfor touched tests. - Run
tofu fmtandtofu validate. - Record the local end-to-end termination validation steps and outcome in this file.
- Request envelope:
{
"version": "v1",
"action": "terminate",
"request_id": "uuid",
"param_prefix": "/devbox",
"payload": {
"identifier": "i-0123456789abcdef0"
}
}payload.identifiermust be a non-empty string and may be either an instance ID or a project name.- Success path event sequence:
- exactly one
resultevent with the termination payload - exactly one terminal
successevent
- exactly one
terminateresult payload shape:
{
"instance_id": "i-0123456789abcdef0",
"project": "my-project"
}2026-05-29: targeted phase 2 pytest suite passed.- Command:
pixi run -e dev python -m pytest tests/commands/test_terminate.py tests/cli_lambda/test_contracts.py tests/cli_lambda/test_app.py tests/test_cli.py tests/test_devbox_manager.py -q - Result:
135 passed, 1 warning
- Command:
2026-05-29: Terraform formatting passed.- Command:
tofu fmt - Result: passed
- Command:
2026-05-29: Terraform validation passed.- Command:
tofu validate - Result: passed
- Command:
- End-to-end commands (must be run by DWHS in devbox-deploy/ repo, not for agents)
tofu plan -out tfplan && tofu apply tfplanto deploy the updated CLI Lambda and IAM policydevbox terminate i-0123456789abcdef0against the deployed CLI Lambda, expecting the existing success messagedevbox terminate my-projectagainst the deployed CLI Lambda, expecting project-name resolution to match current behavior
- Observed outcome: automated validation passed locally; deployed-AWS end-to-end termination validation is still pending operator run.
Milestone: devbox launch ... runs from the local CLI through the deployed CLI Lambda, including userdata handling and current DNS flags.
- Extend the wire contract for
launch, including all current CLI options and the inline userdata payload shape. - Refactor launch logic so Lambda can emit structured progress events instead of relying on raw
printoutput. - Revisit the CLI HTTP timeout policy before
launchgoes remote. Phase 1 wrapsrequeststransport failures cleanly, but still uses a singletimeout=30; decide whether streamed commands need separate connect/read timeouts or a longer read timeout. - Preserve shared business logic; do not fork a second launch implementation just for the Lambda path.
- Keep local preprocessing in the CLI for:
- reading
--userdata-file - embedding file contents into the request payload
- rejecting oversized request bodies before transmission if needed
- reading
- Add the remote
launchaction to the Lambda router. - Expand CLI Lambda IAM only for launch-related operations.
- Migrate only
launchin the CLI to the remote path. - Preserve current flags for DNS behavior and ensure the request contract carries the same semantics.
- Add tests for:
- payload construction for all launch options
- userdata inlining
- progress-event rendering
- launch success and failure mapping
- DNS option propagation
- Run
pixi run -e dev python -m pytestfor touched tests. - Run
tofu fmtandtofu validate. - Record the local end-to-end launch validation steps and outcome in this file.
Milestone: devbox new ... works end-to-end through the deployed CLI Lambda and is no longer dependent on a missing local implementation.
- Implement or restore the shared project-creation core that
newneeds. - Decide the minimal shared interface for project creation so both CLI and Lambda paths use the same logic.
- Extend the wire contract for
new. - Add the remote
newaction to the Lambda router. - Expand CLI Lambda IAM only for project-creation operations.
- Migrate only
newin the CLI to the remote path. - Add tests for:
- project creation success
- duplicate project behavior
- invalid AMI behavior
- invalid project name behavior
- error propagation through the remote path
- Run
pixi run -e dev python -m pytestfor touched tests. - Run
tofu fmtandtofu validate. - Record the local end-to-end
newvalidation steps and outcome in this file.
Milestone: devbox delete-project ... completes the full confirmation and deletion flow through the deployed CLI Lambda.
- Extend the wire contract for a two-step delete flow:
-
delete_project_preflight -
delete_project_execute
-
- Keep local confirmation prompts in the CLI.
- Perform authoritative safety checks in Lambda during preflight.
- Re-check destructive safety conditions again during execute.
- Add the remote delete actions to the Lambda router.
- Expand CLI Lambda IAM only for project deletion and AMI/snapshot cleanup operations.
- Migrate only
delete-projectin the CLI to the remote path. - Preserve current user-facing confirmation semantics as closely as practical.
- Add tests for:
- project not found
- project in use
- prompt cancellation after preflight
- AMI cleanup accepted
- AMI cleanup declined
- execute-time race or safety failure
- remote error propagation
- Run
pixi run -e dev python -m pytestfor touched tests. - Run
tofu fmtandtofu validate. - Record the local end-to-end delete validation steps and outcome in this file.
Milestone: the primary CLI surface is fully Lambda-backed, and obsolete direct-AWS command paths are removed.
- Remove dead local command implementations that are no longer needed in the CLI.
- Consolidate shared helper code and eliminate temporary compatibility scaffolding.
- Review IAM for least privilege after all commands are migrated.
- Update README and any command documentation to describe the Lambda-backed CLI architecture.
- Update tests to remove now-obsolete local-path assumptions.
- Confirm the final acceptance matrix for:
-
status -
terminate -
launch -
new -
delete-project
-
- Run the relevant full test suite with
pixi run -e dev python -m pytest. - Run
tofu fmtandtofu validate. - Record final validation notes, remaining risks, and any explicitly deferred follow-up work in this file.
- Update this file's phase checklist status.
- Add any newly locked decisions and why they were chosen.
- Add any contract changes made in the PR.
- Add the exact manual validation commands that were run and their results.
- Add any known gaps, risks, or follow-up items for the next session.
- Leave a short "next session starts here" note naming the next unchecked work item.
- Each phase is intended to be small enough to fit in a single PR.
- Local validation means the real CLI running from a developer machine against deployed AWS infrastructure for the migrated command.
- Unmigrated commands remain local until their dedicated phase lands.
- Streaming remains in scope throughout the migration; if Lambda Web Adapter proves unworkable in practice, this file must be updated before implementation continues.