Move CLI behind a Lambda: Part 1 - #15
Conversation
Includes: * New lambda terraform for the CLI * Separate docker image for CLI Lambda (because streaming responses require different setup) * Relevant code for lambda in src/devbox/cli_lambda * Helper for local client in src/devbox/remote_client.py * Shared local/remote code defining protocol in src/devbox/cli_protocol * Updates so we use the lambda for the `status` command Assisted-by: Codex:GPT-5.4
Co-local code for a given example. Assisted-by: Codex:GPT-5.4
Assisted-by: Codex:GPT-5.4
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #15 +/- ##
==========================================
+ Coverage 99.75% 99.76% +0.01%
==========================================
Files 13 17 +4
Lines 2818 2938 +120
==========================================
+ Hits 2811 2931 +120
Misses 7 7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR introduces the foundational structure to run CLI commands via a Lambda Function URL (IAM-authenticated + SigV4), and migrates the status command to use the new remote backend while leaving other commands local.
Changes:
- Added a shared wire protocol (
cli_protocol.py), a generic remote invocation client (remote_client.py), and a Starlette-based Lambda app (cli_lambda/) that streams NDJSON events. - Implemented
statusas the first migrated command via a shared command module (commands/status.py) used by both the local CLI and the Lambda handler. - Added Terraform + Docker packaging for the new CLI Lambda, plus unit tests covering contracts, dispatch, remote invocation, and the
statuscommand behavior.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_remote_client.py | Adds unit coverage for signed HTTP invocation and NDJSON parsing/validation behaviors. |
| tests/test_cli.py | Updates CLI tests to patch the new run_status_command path for status. |
| tests/commands/test_status.py | Adds command-level tests spanning payload building, result rehydration, and Lambda handler behavior for status. |
| tests/cli_lambda/test_contracts.py | Adds contract tests for request envelope parsing and NDJSON event encoding. |
| tests/cli_lambda/test_app.py | Adds dispatcher/executor tests for routing and terminal error mapping. |
| src/devbox/remote_client.py | Introduces SigV4-signed Function URL invocation and NDJSON event streaming parser. |
| src/devbox/commands/status.py | Adds shared status implementation for CLI-side invocation + Lambda-side handler and serialization/rehydration. |
| src/devbox/commands/init.py | Creates the new devbox.commands package namespace. |
| src/devbox/cli.py | Migrates the Click status command to delegate to run_status_command. |
| src/devbox/cli_protocol.py | Defines shared protocol constants/enums for actions/events across CLI and Lambda. |
| src/devbox/cli_lambda/contracts.py | Adds request envelope validation and event build/encode helpers for the Lambda HTTP surface. |
| src/devbox/cli_lambda/app.py | Adds Starlette app routing, dispatch table, and NDJSON streaming responses. |
| src/devbox/cli_lambda/init.py | Creates the devbox.cli_lambda package namespace. |
| pyproject.toml | Adds runtime/test deps (requests, responses) and optional deps for the Lambda app (starlette, uvicorn); packages new modules. |
| PLAN-cli-lambda.md | Documents the phased migration plan and the phase 1 contract/validation log. |
| pixi.lock | Locks new Python dependencies added for the remote client and Lambda app tests/runtime. |
| modules/cli-lambda/variables.tf | Defines module inputs for the CLI Lambda deployment. |
| modules/cli-lambda/outputs.tf | Exposes Lambda name, Function URL, and the published SSM parameter name. |
| modules/cli-lambda/main.tf | Builds/pushes the image, provisions Lambda + Function URL, IAM, logging, and SSM publication. |
| main.tf | Wires the new cli_lambda module into the root Terraform stack. |
| lambdas/cli_lambda.Dockerfile | Adds the Lambda container image build (Lambda Web Adapter + uvicorn/Starlette app). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| project = validate_status_payload(envelope.payload) | ||
| manager_prefix = envelope.param_prefix.strip("/") or "devbox" | ||
| manager = DevBoxManager(prefix=manager_prefix) |
There was a problem hiding this comment.
envelope.param_prefix comes from the client request and is used to instantiate DevBoxManager. For future actions that read SSM/Dynamo based on this prefix, this becomes a trust boundary: a caller could supply an unexpected prefix and potentially target different parameters/tables if the Lambda role is permitted. Consider server-side validation/normalization (e.g., restrict to an allowlist or ignore the request value and use a configured prefix) before using it to construct AWS resource names.
There was a problem hiding this comment.
Moved to an issue to be handled later. Marked as a point in the plan doc.
| from enum import StrEnum | ||
|
|
||
| NDJSON_MIME_TYPE = "application/x-ndjson" | ||
| REQUEST_VERSION = "v1" | ||
| FUNCTION_URL_PARAMETER_SUFFIX = "/cli/functionUrl" | ||
|
|
||
|
|
||
| class CliAction(StrEnum): | ||
| """Supported Lambda-backed CLI actions.""" | ||
|
|
||
| STATUS = "status" | ||
|
|
||
|
|
||
| class CliEventType(StrEnum): |
There was a problem hiding this comment.
enum.StrEnum is only available on Python 3.11+. The project metadata currently declares requires-python = ">=3.9" (pyproject.toml), so installing/running on Python 3.10 would fail at import time here. Either avoid StrEnum (use class CliAction(str, Enum): ...) or bump the project’s minimum supported Python version to 3.11+ to match this dependency.
| from enum import StrEnum | |
| NDJSON_MIME_TYPE = "application/x-ndjson" | |
| REQUEST_VERSION = "v1" | |
| FUNCTION_URL_PARAMETER_SUFFIX = "/cli/functionUrl" | |
| class CliAction(StrEnum): | |
| """Supported Lambda-backed CLI actions.""" | |
| STATUS = "status" | |
| class CliEventType(StrEnum): | |
| from enum import Enum | |
| NDJSON_MIME_TYPE = "application/x-ndjson" | |
| REQUEST_VERSION = "v1" | |
| FUNCTION_URL_PARAMETER_SUFFIX = "/cli/functionUrl" | |
| class CliAction(str, Enum): | |
| """Supported Lambda-backed CLI actions.""" | |
| STATUS = "status" | |
| class CliEventType(str, Enum): |
There was a problem hiding this comment.
Addressed in 708c564 by bumping minimum Python version (3.11 is already pretty old).
* Raised minimum Python version to 3.11 * Fixed unused NDJSON_MIME_TYPE import (and downstream importers) * Handled potential `requests` errors * Fixed type annotation in app.py * Removed unused test code * Plan updates for future improvements that are beyond the scope of this PR Assisted-by: Codex:GPT-5.4
ethanholz
left a comment
There was a problem hiding this comment.
Would like to see some docstrings so I have a bit better understanding of some of the validation. Testing and code looks good.
There was a problem hiding this comment.
Would love to see some docstrings for these functions.
There was a problem hiding this comment.
Added in ef5f4e7 (along with other non-CLI, non-test functions in this PR.)
There was a problem hiding this comment.
Would maybe like a line in this plan about using numpy docstrings for all functions in future phases so that these get added during generation.
There was a problem hiding this comment.
Agreed. Added in ef5f4e7 (currently line 21).
Also include this in the plan, so further code will behave the same.
dwhswenson
left a comment
There was a problem hiding this comment.
Ready for another round of review!
There was a problem hiding this comment.
Added in ef5f4e7 (along with other non-CLI, non-test functions in this PR.)
There was a problem hiding this comment.
Agreed. Added in ef5f4e7 (currently line 21).
We want to move our CLI behind a lambda, so that the lambda role can have certain permissions without giving them to arbitrary users.
This is part 1 in that process. This will probably (I hope!) be the most complicated PR of the sequence. In this PR, we lay down the general structure for these, and migrate the
statuscommand to using the lambda backend. Future PRs will migrate the other commands.A detailed plan is committed here in PLAN-cli-lambda.md. Quick summary:
modules/cli-lambdalambda/cli_lambda.Dockerfile. Need a separate Dockerfile because we need to launch a server in order to stream responses back as NDJSON.src/devbox/commands/status.py. Others will move there.src/devbox/cli_lambda/,src/devbox/cli_protocol.py, andsrc/devbox/remote_client.py