Commit cf19e57
authored
feat(runtime): add interactive runtime shell support (#505)
* feat(runtime): add interactive shell support for InvokeAgentRuntimeCommandShell (#133)
* feat(runtime): add interactive terminal SDK for InvokeAgentRuntimeCommandWithWebSocketStream
Adds SDK support for the AgentCore Runtime interactive
terminal API, which streams a real shell session over WebSocket using a
PTY (pseudo-terminal) running inside sandbox-agent on the customer VM.
* fix(runtime): address functional test findings for interactive terminal SDK
- Extract parse_runtime_arn and validate_command_session_id as module-level
functions; remove private instance methods _parse_runtime_arn and
_validate_command_session_id
- Add region mismatch check in open_terminal() and TerminalSession.__init__
so a client/ARN region mismatch raises ValueError immediately instead of
producing a confusing 404 at connect time
- Reject non-str command_session_id at construction (isinstance guard before
regex match)
- Use \Z anchor in _COMMAND_SESSION_ID_RE to reject trailing newline
- Add eager validation in open_terminal() and TerminalSession.__init__
- Log InvalidStatus response body on WebSocket upgrade failure (HTTP 4xx/5xx)
- Short-circuit _reconnect_with_backoff when reconnect_window <= 0
- Fix reconnected/kicked flags reset on each _connect() call
- Change raise StopAsyncIteration inside except clauses to raise ... from None
(B904) and wrap long logger calls (E501) throughout terminal.py
- Add r-prefix to docstrings containing backslashes (D301) in command_stream.py
and terminal.py
- Pin griffe<2 and fix ExplanationStyle.MARKDOWN casing in breaking-change
workflow (griffe 2.0 removed the lowercase enum alias)
- Add 38 new unit tests covering the above; 188 unit tests pass
* refactor(runtime): reorganize shell feature into runtime/shell/ subpackage
Rename all terminal/command-stream symbols to shell/shell_id to match the
renamed service API (InvokeAgentRuntimeCommandShell). Reorganize the flat
shell.py and shell_protocol.py into a structured subpackage with separate
modules for auth, config, protocol, session, and validation.
Wire-level parameter commandSessionId is intentionally unchanged — the
service API has not yet been updated.
* fix(runtime/shell): address code review findings
- Raise StopAsyncIteration from None consistently across all 8 sites
- Reset bytes_dropped to 0 in _connect() to reflect most recent disconnect
- Guard bearer token length before embedding in Sec-WebSocket-Protocol (max 4096 chars encoded)
- Expose session_id as a public attribute alongside shell_id
- Log WARNING (not silent stop) when server sends 1001 Going Away without reconnect_config
- Reject float width/height in encode_resize() with a clear TypeError
- Remove duplicate validate_shell_id() call in open_shell() (ShellSession.__init__ already validates)
* chore(runtime/shell): apply ruff lint and format fixes
Fix import sort order and line length in runtime/__init__.py;
reformat shell subpackage files to satisfy ruff-format.
* chore: apply ruff lint and format fixes across src/ and tests/
Fix import sort, line length, and formatting across all Python files
to satisfy pre-commit ruff checks in CI.
* fix(runtime/shell): address PR review findings
- Filter HEARTBEAT frames in __anext__ (server echoes 0x05 back; wire-level
keepalive must not be surfaced to caller iterators)
- Remove bytes_dropped reset in _connect(): server reports a cumulative
monotonic count, so resetting creates a false zero window before the
post-drain confirmation arrives
- Fix test_pending_frames_cleared_on_reconnect: previous assertion accepted
["stale","fresh"] whether or not clearing existed; reworked to directly
verify _pending_frames is empty after _connect() is called on reconnect
* feat(runtime/shell): expose exit_code on ShellSession
Adds `ShellSession.exit_code: Optional[int]` so callers can check the
shell process exit status after the async-for loop drains.
- `None` until the termination STATUS frame arrives
- `0` for a clean exit, non-zero for failed commands or signal-killed
processes (e.g. 137 for SIGKILL)
- New `_parse_exit_code` static helper extracts the code from the
metav1.Status JSON causes list, falling back to 0 for platform errors
that lack an ExitCode cause
- Both termination paths (pending-frames drain and main recv loop) set
the attribute before marking `_closed = True`
* test(runtime/shell): add unit tests for ShellSession.exit_code
Five tests in TestShellSessionExitCode:
- clean exit (status=Success) → exit_code 0
- non-zero exit code extracted from ExitCode cause
- exit_code is None during STDOUT frames, set only on termination STATUS
- exit_code set via pending-frames drain path (stashed STDOUT + exit STATUS)
- platform InternalError without ExitCode cause falls back to 0
* fix(runtime/shell): return None from _parse_exit_code when no ExitCode cause present
Returning 0 as the fallback was ambiguous — callers could not distinguish
a clean shell exit from a platform InternalError that carried no exit code.
Changed _parse_exit_code return type to Optional[int] and the fallback
from 0 to None. Updated docstring and the corresponding unit test.
* fix(runtime/shell): auto-generate session_id in ShellSession.__init__ (#135)
* fix(runtime/shell): auto-generate session_id in ShellSession.__init__
Previously self._session_id stored None when the caller omitted session_id.
Each _connect() call then passed None to connect_shell(), which generated a
fresh UUID each time — routing to a different VM on every reconnect, so
shell_id was never found and reconnected was always False.
Fix mirrors the existing shell_id pattern: generate once in __init__ and
reuse the same value across all _connect() calls. Also tightens the public
attribute type from Optional[str] to str since session_id is now always set.
Drop _shell_id/_session_id backing fields. Both public attributes
(shell_id, session_id) are the single source of truth.
Also reads X-Amzn-Bedrock-AgentCore-Runtime-Session-Id from the 101
response to surface the DP-confirmed session ID, same as shell_id already
does via X-Command-Session-Id.
* test(runtime/shell): add unit tests for session_id fixes
- Fix _make_ws() to set ws.response.headers={} — prevents AutoMock.get()
returning a truthy coroutine that silently overwrites shell_id/session_id
in every existing test (also dropped 67 spurious coroutine warnings)
- Add test_session_id_stable_across_two_connect_calls: verifies session_id
is unchanged across reconnects so all _connect() calls route to same VM
- Add test_connect_reads_session_id_from_101_header: verifies
X-Amzn-Bedrock-AgentCore-Runtime-Session-Id updates session_id
- Add test_connect_timeout_proceeds_with_warning headers stub
- Add TestShellSessionInit tests: None auto-generates UUID, "" auto-generates
UUID, explicit value is preserved
* fix(runtime/shell): update wire names for InvokeAgentRuntimeCommandShell API (#136)
* fix(runtime/shell): update wire names for InvokeAgentRuntimeCommandShell API
Path: /ws/commands → /ws/shells
Query param: commandSessionId → shellId
101 header: X-Command-Session-Id → X-Amzn-Bedrock-AgentCore-Shell-Id
STATUS frame wire protocol: commandSessionId → shellId
* fix(runtime/shell): update remaining ws/commands references to ws/shells
Update stale /ws/commands references in docstring and test fixtures
that were missed in the prior wire rename commit.
* docs(runtime/shell): clarify that both shell_id and session_id are required for reconnect (#139)
The previous docstrings only mentioned shell_id in reconnect examples,
omitting session_id. Without a pinned session_id the SDK auto-generates
a fresh UUID on each open_shell call, routing to a different VM where
the PTY no longer exists (reconnected=False).
* chore(runtime/shell): fix ruff import order in session.py
Move `from ..models` import above relative imports to satisfy isort.
* revert(ci): restore griffe==1.7.3 pin and b.explain() in breaking-change workflow
The griffe change was bundled into the shell feature cherry-pick but
belongs to the private repo only. Restore the public repo's original
pinned version and call style.1 parent 69954eb commit cf19e57
15 files changed
Lines changed: 3288 additions & 170 deletions
File tree
- src/bedrock_agentcore/runtime
- shell
- tests/unit/runtime
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
13 | 23 | | |
14 | 24 | | |
15 | 25 | | |
16 | 26 | | |
| 27 | + | |
17 | 28 | | |
18 | 29 | | |
19 | | - | |
20 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
21 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
22 | 40 | | |
23 | 41 | | |
24 | 42 | | |
| |||
0 commit comments