Skip to content

Commit 09fe5fd

Browse files
committed
Merge remote-tracking branch 'upstream/main' into lv-sync-main-2026-07-23b
# Conflicts: # agent/pyproject.toml # agent/src/pipeline.py # agent/src/shell.py # agent/tests/conftest.py # agent/tests/test_aws_session.py # agent/tests/test_shell.py # cdk/src/constructs/ecs-agent-cluster.ts # cdk/src/constructs/task-table.ts # cdk/src/handlers/shared/create-task-core.ts # cdk/src/handlers/shared/error-classifier.ts # cdk/src/handlers/shared/types.ts # cdk/test/constructs/task-table.test.ts # cdk/test/handlers/shared/error-classifier.test.ts # cdk/test/stacks/agent.test.ts
2 parents 3e73d5a + d3923ad commit 09fe5fd

69 files changed

Lines changed: 4311 additions & 143 deletions

Some content is hidden

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

.github/workflows/deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ jobs:
153153
github-token: ${{ github.token }}
154154

155155
- name: Configure AWS credentials
156-
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
156+
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
157157
with:
158158
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
159159
aws-region: ${{ vars.AWS_REGION }}
@@ -228,7 +228,7 @@ jobs:
228228
github-token: ${{ github.token }}
229229

230230
- name: Configure AWS credentials
231-
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
231+
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
232232
with:
233233
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
234234
aws-region: ${{ vars.AWS_REGION }}

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/integ.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ jobs:
199199
persist-credentials: false
200200

201201
- name: Configure AWS credentials
202-
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
202+
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
203203
with:
204204
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
205205
# Fall back to us-east-1 if the repo variable is unset, so the action

.github/workflows/monthly-repo-metrics.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
4242
4343
- name: Report on issues
44-
uses: github/issue-metrics@44173f9e0a3b2144a777a10a340e4c09a25ac9f8 # v4.2.8
44+
uses: github/issue-metrics@df8c49d20958f9345281fa2124858bd0ad227e1f # v5.0.0
4545
env:
4646
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4747
SEARCH_QUERY: repo:aws-samples/sample-autonomous-cloud-coding-agents is:issue created:${{ env.last_month }} -reason:"not planned"
@@ -54,7 +54,7 @@ jobs:
5454
content-filepath: ./issue_metrics.md
5555

5656
- name: Report on PRs
57-
uses: github/issue-metrics@44173f9e0a3b2144a777a10a340e4c09a25ac9f8 # v4.2.8
57+
uses: github/issue-metrics@df8c49d20958f9345281fa2124858bd0ad227e1f # v5.0.0
5858
env:
5959
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6060
SEARCH_QUERY: repo:aws-samples/sample-autonomous-cloud-coding-agents is:pr created:${{ env.last_month }} -is:draft

agent/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ARG GH_VERSION=2.93.0
44
FROM --platform=$TARGETPLATFORM jdxcode/mise:latest AS mise
55

66
# Build gh with a patched Go toolchain; upstream packages can lag Go CVE fixes.
7-
FROM --platform=$TARGETPLATFORM golang:1.26.3-bookworm AS gh-builder
7+
FROM --platform=$TARGETPLATFORM golang:1.26.4-bookworm AS gh-builder
88
ARG GH_VERSION
99
RUN GOPROXY=direct GOBIN=/out go install "github.com/cli/cli/v2/cmd/gh@v${GH_VERSION}"
1010

agent/tests/conftest.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,29 @@
1414
# in the MAIN thread during a test's *call* phase, so a deadlock in a WORKER
1515
# thread, a fixture, collection, or a C-level socket read the main thread never
1616
# returns from stalls the whole `mise run build` silently — up to the platform's
17-
# 3600s build-verify ceiling (the ECS-only stall on ABCA-684/686/688, and again
18-
# on ABCA-707: 40+ min of dead air, container never reaped).
17+
# 3600s build-verify ceiling (the ECS-only stall on ABCA-684/686/688, and the
18+
# scoped-session S3 hang the _clean_env reset below guards against: 40+ min of
19+
# dead air, container never reaped).
1920
#
2021
# The obvious instrument — ``faulthandler.dump_traceback_later(1200, exit=True)``
2122
# — does NOT work here: faulthandler has a SINGLE internal timer, and pytest's
2223
# ``faulthandler_timeout`` (pyproject.toml) RE-ARMS it at the start of every test
2324
# WITHOUT ``exit=True``. So a session-level exit timer is cancelled by the first
24-
# test, the per-test timer only DUMPS, and the suite hangs forever anyway
25-
# (exactly what happened on ABCA-707: a 300s dump fired, no exit followed).
25+
# test, the per-test timer only DUMPS, and the suite hangs forever anyway.
2626
#
2727
# So own the reaper on a dedicated daemon thread pytest cannot touch. A blocked
28-
# socket read (the ABCA-707 failure mode) releases the GIL, so this thread runs;
29-
# it dumps every thread's stack for diagnosis and then HARD-EXITS the process, so
30-
# `mise run build` returns non-zero within seconds of the deadline instead of
31-
# burning to the 3600s ceiling. Deadline 600s: ~200x the whole suite's normal
32-
# ~3s… well above any legitimate run (per-test cap is 300s) yet far under the
33-
# build ceiling.
28+
# socket read releases the GIL, so this thread runs; it dumps every thread's
29+
# stack for diagnosis and then HARD-EXITS the process, so `mise run build`
30+
# returns non-zero within seconds of the deadline instead of burning to the
31+
# ceiling. Deadline 600s: a SESSION backstop for the whole-suite hangs SIGALRM
32+
# can't interrupt — sized well above the longest healthy run (the suite normally
33+
# finishes in seconds; the per-test pytest-timeout cap is 120s, see
34+
# pyproject.toml) yet far under the 3600s build-verify ceiling.
35+
#
36+
# ``pytest_sessionfinish`` cancels the timer on a clean finish (below), so a
37+
# legitimately slow-but-passing run that lands near 600s — e.g. still in teardown
38+
# / coverage write — is NOT hard-exited into a bewildering red (#616 review N2).
39+
# ``os._exit`` skips atexit + buffer flush, so it must only fire on a TRUE hang.
3440
_HANG_REAP_DEADLINE_S = 600
3541

3642

@@ -52,6 +58,17 @@ def _reap_on_hang() -> None:
5258
_hang_watchdog.start()
5359

5460

61+
def pytest_sessionfinish(session, exitstatus):
62+
"""Cancel the hang watchdog on a clean session finish (#616 review N2).
63+
64+
Without this, a legitimately slow-but-passing suite that finishes just after
65+
the 600s deadline (e.g. during teardown / coverage write) would be hard-exited
66+
by ``_reap_on_hang`` and turn green red with a thread-dump uncorrelated to any
67+
failed test. ``Timer.cancel()`` is a no-op if the timer already fired (a true
68+
hang), so this only prevents the false-positive kill."""
69+
_hang_watchdog.cancel()
70+
71+
5572
class FakeRunCmd:
5673
"""Shared fake for ``shell.run_cmd``: records argv and returns scripted results.
5774
@@ -139,15 +156,9 @@ def make_task_config(**overrides) -> TaskConfig:
139156
"LOG_GROUP_NAME",
140157
"MEMORY_ID",
141158
"ENABLE_CLI_TELEMETRY",
142-
# Per-session IAM scoping (PR #209). When this is set, ``aws_session`` resolves
143-
# a *scoped* session and ``tenant_client`` returns ``session.client(...)`` —
144-
# which BYPASSES a ``@patch("boto3.client")`` mock. On the ECS substrate the
145-
# task def sets this, so a test that mocks ``boto3.client`` (e.g.
146-
# ``test_attachments``) instead makes a REAL S3 call that hangs on the network
147-
# (no egress). Stripping it here forces every test onto the unscoped path,
148-
# where the ``boto3.client`` mock actually intercepts. Resetting the session
149-
# cache below is NOT enough on its own — a fresh ``get_session()`` re-resolves
150-
# as scoped while this var is still set. (Live-caught on ABCA-707, 2026-07-15.)
159+
# Per-session IAM scoping (PR #209) — the scoped-session S3-hang guard.
160+
# See the ``_clean_env`` docstring below for the full rationale (why the
161+
# env strip AND the cache reset are both required).
151162
"AGENT_SESSION_ROLE_ARN",
152163
]
153164

@@ -156,9 +167,6 @@ def make_task_config(**overrides) -> TaskConfig:
156167
def _clean_env(monkeypatch):
157168
"""Remove agent-related env vars and reset the AWS session cache each test.
158169
159-
The env cleanup keeps host state from leaking into agent code that reads
160-
``os.environ`` at import/call time.
161-
162170
The env cleanup + session reset TOGETHER close a scoped-session leak that
163171
hangs the suite on the ECS substrate: ``aws_session`` caches the resolved
164172
boto3 session in a MODULE GLOBAL (``_session``/``_scoped``), and
@@ -171,7 +179,7 @@ def _clean_env(monkeypatch):
171179
the var gone AND the cache reset, every test resolves the unscoped path where
172180
its ``boto3.client`` mock intercepts. Otherwise a mocked test (e.g.
173181
``test_attachments``) makes a REAL S3 call that hangs on the ECS network
174-
(no egress) in a socket read SIGALRM can't interrupt. (Live-caught ABCA-707.)
182+
(no egress) in a socket read SIGALRM can't interrupt.
175183
"""
176184
for var in _AGENT_ENV_VARS:
177185
monkeypatch.delenv(var, raising=False)

agent/tests/test_aws_session.py

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -79,33 +79,13 @@ def test_blank_role_arn_treated_as_unset(self, monkeypatch):
7979
assert is_scoped() is False
8080

8181

82-
class TestConftestScrubsScopingEnv:
83-
"""Regression guard for the ECS test-hang (ABCA-707).
84-
85-
The bug: the ECS agent task def sets ``AGENT_SESSION_ROLE_ARN``. If that var
86-
leaks into the test process, ``get_session()`` resolves a *scoped* session and
87-
``tenant_client`` returns ``session.client(...)`` — bypassing a
88-
``@patch("boto3.client")`` mock. A mocked test (e.g. ``test_attachments``)
89-
then makes a REAL S3 call that hangs on the ECS network (no egress) in a
90-
socket read SIGALRM can't interrupt → the whole ``mise run build`` stalls
91-
silently for 40+ min. The ``_clean_env`` autouse fixture in conftest MUST
92-
strip the var (resetting the session cache alone is not enough — a cold
93-
``get_session()`` re-resolves scoped while the var is still set).
94-
"""
95-
96-
def test_session_role_arn_not_visible_to_tests(self):
97-
import os
98-
99-
# No monkeypatch here: this asserts the AUTOUSE fixture already scrubbed
100-
# the var, even if the parent (ECS) environment had it set.
101-
assert os.environ.get(SESSION_ROLE_ARN_ENV) is None
102-
103-
def test_session_resolves_unscoped_by_default(self):
104-
# With the var scrubbed and the cache reset (both by _clean_env), a bare
105-
# get_session() must be unscoped — the path where boto3.client mocks work.
106-
with patch("boto3.Session", return_value=MagicMock()):
107-
get_session()
108-
assert is_scoped() is False
82+
# NOTE: the conftest-scrub regression guard for AGENT_SESSION_ROLE_ARN lives in
83+
# tests/test_conftest_env_scrub.py — deliberately in its OWN module with NO local
84+
# autouse fixture. This file's `_reset` fixture (above) itself does
85+
# `monkeypatch.delenv(SESSION_ROLE_ARN_ENV)`, which would MASK whether conftest's
86+
# `_clean_env` performs the scrub — a guard placed here passes even if the
87+
# conftest line is deleted (#616 review B1). Only a fixture-free module truly
88+
# guards the fix.
10989

11090

11191
# ---------------------------------------------------------------------------
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Fixture-free regression guard for the ECS test-hang scrub (#615 / #616 B1).
2+
3+
This module deliberately has NO local autouse fixture. It exists to prove that
4+
conftest's ``_clean_env`` autouse fixture strips ``AGENT_SESSION_ROLE_ARN`` from
5+
the environment before each test — the exact line the #615 fix adds to
6+
``_AGENT_ENV_VARS``.
7+
8+
Why a separate module: ``test_aws_session.py`` has its OWN autouse ``_reset``
9+
fixture that does ``monkeypatch.delenv(SESSION_ROLE_ARN_ENV)``, so a guard placed
10+
there passes even if the conftest scrub is deleted (it asserts what the local
11+
fixture guarantees, not what the fix does — #616 review B1). Here, only conftest's
12+
``_clean_env`` is in play.
13+
14+
Why set the var at MODULE IMPORT time (not in the test body): pytest autouse
15+
fixtures run during test *setup*, before the body executes — so a
16+
``monkeypatch.setenv`` inside the test can't be scrubbed by them. Poisoning the
17+
process env at import time (before any fixture runs) means the conftest scrub is
18+
the only thing that can clear it by the time a test body reads it. Remove the
19+
``AGENT_SESSION_ROLE_ARN`` line from conftest's ``_AGENT_ENV_VARS`` and BOTH tests
20+
below fail — a true bidirectional guard.
21+
22+
The bug this guards: with ``AGENT_SESSION_ROLE_ARN`` set, ``aws_session``
23+
resolves a *scoped* session and ``tenant_client`` returns ``session.client(...)``,
24+
bypassing a ``@patch("boto3.client")`` mock. A mocked test (e.g.
25+
``test_attachments``) then makes a REAL S3 call that hangs forever on the ECS
26+
network (no egress) in a socket read SIGALRM can't interrupt — stalling the whole
27+
``mise run build``.
28+
"""
29+
30+
import os
31+
32+
from aws_session import SESSION_ROLE_ARN_ENV, is_scoped
33+
34+
# Poison the process env at import time — BEFORE any fixture runs. The conftest
35+
# `_clean_env` autouse must scrub this for the assertions below to hold.
36+
os.environ[SESSION_ROLE_ARN_ENV] = "arn:aws:iam::123456789012:role/leaked-from-parent-env"
37+
38+
39+
def test_conftest_scrubs_session_role_arn():
40+
# If conftest's _clean_env strips AGENT_SESSION_ROLE_ARN, it is gone here even
41+
# though the module poisoned it at import time. If the scrub line is removed,
42+
# this fails — the guard the #615 fix actually needs.
43+
assert os.environ.get(SESSION_ROLE_ARN_ENV) is None
44+
45+
46+
def test_session_resolves_unscoped_when_scrubbed():
47+
# With the var scrubbed (and the cache reset, both by conftest _clean_env) a
48+
# bare get_session() must be unscoped — the path where boto3.client mocks
49+
# intercept, so no real S3 call is made and nothing hangs.
50+
assert is_scoped() is False

0 commit comments

Comments
 (0)