Repository navigation
feat(deploy): Run a CodeZip agent in a box on AgentCore Runtime #2792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev-box-run
Are you sure you want to change the base?
Changes from all commits
8b7aed0
da5d453
053f603
03e2028
06ea0dc
a5a5a81
4211fe1
4eb14e5
f98580f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,4 +38,7 @@ env/ | |
|
|
||
| # OS | ||
| .DS_Store | ||
| Thumbs.db | ||
| Thumbs.db | ||
|
|
||
| # Box, which agentcore deploy stages for AgentCore Runtime | ||
| .box-dist/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,4 +38,7 @@ env/ | |
|
|
||
| # OS | ||
| .DS_Store | ||
| Thumbs.db | ||
| Thumbs.db | ||
|
|
||
| # Box, which agentcore deploy stages for AgentCore Runtime | ||
| .box-dist/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,4 +38,7 @@ env/ | |
|
|
||
| # OS | ||
| .DS_Store | ||
| Thumbs.db | ||
| Thumbs.db | ||
|
|
||
| # Box, which agentcore deploy stages for AgentCore Runtime | ||
| .box-dist/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,4 +38,7 @@ env/ | |
|
|
||
| # OS | ||
| .DS_Store | ||
| Thumbs.db | ||
| Thumbs.db | ||
|
|
||
| # Box, which agentcore deploy stages for AgentCore Runtime | ||
| .box-dist/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Starts the agent in a box on AgentCore Runtime. The Runtime's entry point must be a .py file, so | ||
| # this hands over to start.sh, and tells it which Python the Runtime runs. | ||
| import os | ||
| import sys | ||
|
|
||
| script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "start.sh") | ||
| os.execve("/bin/bash", ["bash", script], {**os.environ, "BOX_START_PYTHON": sys.executable}) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| #!/bin/bash | ||
| # Runs the agent in a box on AgentCore Runtime (CodeZip). start.py runs this script. Written by | ||
| # `agentcore create`; `agentcore deploy` stages .box-dist/ beside it. | ||
| # | ||
| # The microVM (Amazon Linux 2023, arm64, an ordinary user, HOME unset): | ||
| # | ||
| # /var/task/ the zip, read-only $CODE | ||
| # ├── start.py start.sh main.py main.py is the agent | ||
| # ├── <dependencies> installed at the zip root | ||
| # └── .box-dist/ staged by agentcore deploy | ||
| # ├── box strands-box-sock-alias strands-box-contain-trampoline | ||
| # ├── resources/ box's packs | ||
| # ├── ffi/ _ctypes and a libffi that Box Core's W^X filter allows | ||
| # └── profile.toml rendered from agentcore.json, with @CODE@, @PYTHON@, | ||
| # @PYTHON_PREFIX@ and @GATEWAY_BOX_URL@ for this script | ||
| # | ||
| # /tmp/box/ written here; gone with the microVM | ||
| # ├── profile.toml the filled profile, which box run takes by path $PROFILE | ||
| # ├── workspace/ where box run starts; the agent reads and writes $WORKSPACE | ||
| # └── home/ HOME for box; the agent cannot read it $HOME_DIR | ||
| # | ||
| # Box signs the AWS requests with the execution role: a bare aws:// in the profile resolves the AWS | ||
| # default credential chain, which Box Core reads and refreshes itself. | ||
| # | ||
| # If box fails, or the agent in it does not answer by 22s, this serves main.py without box: the | ||
| # Runtime allows 30s to start. Set BOX_START_DISABLE=1 on the Runtime to skip box. | ||
| set -u | ||
|
|
||
| CODE=$(cd "$(dirname "$0")" && pwd) | ||
| PYTHON=${BOX_START_PYTHON:-$(command -v python3)} | ||
| DIST=$CODE/.box-dist | ||
| BOX=$DIST/box | ||
| WORKSPACE=/tmp/box/workspace | ||
| HOME_DIR=/tmp/box/home | ||
| # Outside the workspace, so box accepts the profile file. | ||
| PROFILE=/tmp/box/profile.toml | ||
| READY_BY=22 | ||
|
|
||
| log() { echo "[box] $*"; } | ||
|
|
||
| # A Python for helper scripts. Setting PYTHONPATH drops the OpenTelemetry startup hooks that | ||
| # opentelemetry-instrument adds: they cost about 3s and start threads. | ||
| pyq() { env PYTHONPATH="$CODE" "$PYTHON" "$@"; } | ||
|
|
||
| serve_without_box() { | ||
| log "FALLBACK ($1) after ${SECONDS}s: serving the agent without box" | ||
| cd "$CODE" && exec "$PYTHON" main.py | ||
| } | ||
|
|
||
| # --- The profile ----------------------------------------------------------------------------------- | ||
| write_profile() { | ||
| local python_prefix agent_env | ||
| python_prefix=$(pyq -c 'import sys; print(sys.base_prefix)') | ||
|
|
||
| # Only the deployed Memories' and Gateways' IDs and URLs reach the agent, never a secret. | ||
| agent_env=$(env | grep -E '^((AGENTCORE_GATEWAY|MEMORY)_[A-Z0-9_]+_(URL|ARN|ID|AUTH_TYPE)|AWS_REGION|AWS_DEFAULT_REGION)=[!-~]*$' \ | ||
| | sed -E 's/^([^=]+)=(.*)$/\1 = "\2"/') | ||
|
|
||
| # The Box gateway's URL exists only after deploy; the CDK sets it on the Runtime. | ||
| sed -e "s|@CODE@|$CODE|g" -e "s|@PYTHON_PREFIX@|$python_prefix|g" -e "s|@PYTHON@|$PYTHON|g" \ | ||
| -e "s|@GATEWAY_BOX_URL@|${AGENTCORE_GATEWAY_BOX_URL:-}|g" \ | ||
| "$DIST/profile.toml" \ | ||
| | awk -v lines="$agent_env" '$0 == "# @AGENT_ENV@" { if (lines != "") print lines; next } { print }' \ | ||
| > "$PROFILE" | ||
|
|
||
| } | ||
|
|
||
| # --- Start box ------------------------------------------------------------------------------------- | ||
| # Box opens the ingress port before the agent is up, so an answer from the agent through it is the | ||
| # sign of ready. Any HTTP status line counts: the agent answered, whatever its paths are. | ||
| agent_ready() ( | ||
| exec 3<>"/dev/tcp/127.0.0.1/$1" || exit 1 | ||
| printf 'GET /ping HTTP/1.0\r\nHost: localhost\r\n\r\n' >&3 | ||
| read -r -t 2 status <&3 && [[ "$status" == HTTP/* ]] | ||
| ) 2>/dev/null | ||
|
|
||
| start_box() { | ||
| local port pid code | ||
| port=$(grep -m1 -oE '^listen = "[0-9.]+:[0-9]+"' "$PROFILE" | grep -oE '[0-9]+"$' | tr -d '"') | ||
| cd "$WORKSPACE" || serve_without_box "no workspace" | ||
| log "starting box run (ingress :${port:-8080})" | ||
| # The prefix keeps box's own lines in a "[box]" log query. | ||
| env HOME="$HOME_DIR" "$BOX" run "$PROFILE" --no-banner > >(sed -u 's/^/[box] box: /') 2>&1 & | ||
| pid=$! | ||
| while [ "$SECONDS" -lt "$READY_BY" ]; do | ||
| if ! kill -0 "$pid" 2>/dev/null; then | ||
| wait "$pid" | ||
| serve_without_box "box run exited with $?" | ||
| fi | ||
| if agent_ready "${port:-8080}"; then | ||
| log "the agent in box answers after ${SECONDS}s" | ||
| trap 'kill -TERM "$pid"' TERM INT | ||
| wait "$pid" | ||
| code=$? | ||
| log "box run exited with $code" | ||
| exit "$code" | ||
| fi | ||
| sleep 0.5 | ||
| done | ||
| kill "$pid" 2>/dev/null | ||
| serve_without_box "the agent in box did not answer by ${READY_BY}s" | ||
| } | ||
|
|
||
| # --- Main -------------------------------------------------------------------------------------------- | ||
| [ "${BOX_START_DISABLE:-}" = 1 ] && serve_without_box "BOX_START_DISABLE=1" | ||
| [ -x "$BOX" ] && [ -f "$DIST/profile.toml" ] || serve_without_box "no $DIST; deploy stages it" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High (design): fails open. Every path here, and the 22s timeout and — 🤖 Abhilash's Claude Bot |
||
| log "box: $("$BOX" --version 2>&1 | head -1) python: $PYTHON user: $(id -u)" | ||
|
|
||
| mkdir -p "$WORKSPACE" "$HOME_DIR" | ||
| chmod 0700 "$HOME_DIR" | ||
|
|
||
| write_profile | ||
| log "profile:" | ||
| sed 's/^/[box] /' "$PROFILE" | ||
| start_box | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,4 +38,7 @@ env/ | |
|
|
||
| # OS | ||
| .DS_Store | ||
| Thumbs.db | ||
| Thumbs.db | ||
|
|
||
| # Box, which agentcore deploy stages for AgentCore Runtime | ||
| .box-dist/ | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium: port race.
kill "$pid"sends SIGTERM but does not wait, soserve_without_boxexecsmain.pyright away. Box opened the ingress port (0.0.0.0:8080for HTTP) before the agent was up, andmain.pybinds that same port. If box has not closed its listener yet, the fallback dies with EADDRINUSE and the Runtime gets nothing. The agent child that box started on the workload port may also outlive box. Suggestkill "$pid"; wait "$pid" 2>/dev/nullwith a short bounded wait, escalating tokill -KILLif needed. That matters even more if finding 2 keeps any fallback.— 🤖 Abhilash's Claude Bot