alpha-engine-lib 0.34.0__tar.gz → 0.35.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/PKG-INFO +35 -6
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/README.md +33 -4
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/pyproject.toml +2 -2
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/__init__.py +1 -1
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/pipeline_status/read.py +25 -1
- alpha_engine_lib-0.35.0/src/alpha_engine_lib/ssm_dispatcher.py +463 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib.egg-info/PKG-INFO +35 -6
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib.egg-info/SOURCES.txt +2 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_pipeline_status_read.py +22 -0
- alpha_engine_lib-0.35.0/tests/test_ssm_dispatcher.py +656 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/setup.cfg +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/agent_schemas.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/alerts.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/arcticdb.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/collector_results.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/cost.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/dates.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/decision_capture.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/ec2_spot.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/email_sender.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/eval_artifacts.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/logging.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/model_pricing.yaml +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/pillars.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/pipeline_status/__init__.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/pipeline_status/registry.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/pipeline_status/templates.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/preflight.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/__init__.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/db.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/embeddings.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/migrations/0001_content_tsv.sql +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/rerank.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/retrieval.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/rag/schema.sql +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/reconcile.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/secrets.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/sources/__init__.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/sources/protocols.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/ssm_log_capture.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/telegram.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/trading_calendar.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/transparency.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/transparency_inventory.yaml +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/universe.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib.egg-info/dependency_links.txt +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib.egg-info/requires.txt +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib.egg-info/top_level.txt +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_agent_schemas.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_alerts.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_arcticdb.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_collector_results.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_cost.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_dates.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_decision_capture.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_ec2_spot.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_email_sender.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_eval_artifacts.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_logging.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_pillars.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_pipeline_status_registry.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_pipeline_status_templates.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_preflight.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_rag.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_rag_rerank.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_rag_retrieval_hybrid.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_reconcile.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_secrets.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_sources_protocols.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_ssm_log_capture.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_telegram.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_trading_calendar.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_transparency.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_universe.py +0 -0
- {alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/tests/test_version_pin.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-engine-lib
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
|
|
3
|
+
Version: 0.35.0
|
|
4
|
+
Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
|
|
5
5
|
Author: Brian McMahon
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.9
|
|
@@ -181,18 +181,47 @@ results = retrieve(
|
|
|
181
181
|
|
|
182
182
|
Requires the `[rag]` extra. Embeddings are Voyage `voyage-3-lite` (512d); the database backend is Neon Postgres with pgvector + HNSW indexes.
|
|
183
183
|
|
|
184
|
+
### `ssm_dispatcher` — SSM send-command + poll chokepoint
|
|
185
|
+
|
|
186
|
+
Canonical Python primitive for the `run_ssm` bash helper that previously appeared as a ~54-line mirror across each dispatcher script that drives a spot instance over the SSM transport. The pre-lift shape — base64-wrap the script body, `aws ssm send-command --document-name AWS-RunShellScript`, loop on `get-command-invocation`, stream the `StandardOutputContent` delta, propagate the inner exit — now lives in one place where the polling cadence, error-class handling, and S3 output-key layout match across every consumer.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
python -m alpha_engine_lib.ssm_dispatcher run \
|
|
190
|
+
--instance-id "$INSTANCE_ID" \
|
|
191
|
+
--description "bootstrap" \
|
|
192
|
+
--timeout 3600 \
|
|
193
|
+
--output-bucket "$S3_BUCKET" \
|
|
194
|
+
--output-key-prefix "${S3_STAGING_PREFIX}/ssm-output" \
|
|
195
|
+
--region "$AWS_REGION" \
|
|
196
|
+
--script-stdin <<'BOOTSTRAP'
|
|
197
|
+
set -eo pipefail
|
|
198
|
+
export HOME=/home/ec2-user AWS_REGION=us-east-1
|
|
199
|
+
# ...the script body the SSM target will execute...
|
|
200
|
+
BOOTSTRAP
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Exit 0 on `Success`; exit 1 on any terminal non-Success status, send-command failure, or unrecoverable poll failure; exit 2 on bad CLI input. `InvocationDoesNotExist` during the first 60s after SendCommand counts as a registration race and keeps polling — closes the 2026-05-23 Saturday SF substrate weakness at the chokepoint rather than per-SF-JSON Retry block.
|
|
204
|
+
|
|
205
|
+
### `ssm_log_capture` — SSM-step log capture + S3 ship-on-exit chokepoint
|
|
206
|
+
|
|
207
|
+
Pairs with `ssm_dispatcher` on the SSM target side. The dispatcher script tells the target instance to invoke `python -m alpha_engine_lib.ssm_log_capture run --slug X --log /var/log/X.log -- bash <launcher>`; the target wrapper tees the launcher's stdout/stderr to a local log file and to its own stdout (so the SSM `StandardOutputContent` channel still surfaces output to the dispatcher), then on exit ships the full local log to `s3://alpha-engine-research/_ssm_logs/{slug}/{date}/{host}-{time}.log` regardless of the inner exit code. Replaces the inline `trap 'aws s3 cp ...' EXIT` pattern that broke under ASL `States.Array` escape semantics (2026-05-22 Friday-PM dry-pass catch).
|
|
208
|
+
|
|
209
|
+
### `ec2_spot` — capacity-resilient spot-launch chokepoint
|
|
210
|
+
|
|
211
|
+
Rotates across `(instance_type × subnet)` combinations on `InsufficientInstanceCapacity` / `InsufficientHostCapacity` / `Unsupported` / `InvalidAvailabilityZone` / `SpotMaxPriceTooLow`; non-capacity errors raise immediately. CLI exit 64 distinguishes capacity exhaustion from generic failure. Replaces the hardcoded single-subnet + single-instance-type launch pattern that mirrored across each dispatcher; landed 2026-05-22 after the third-recurrence-in-a-month spot-launch fragility.
|
|
212
|
+
|
|
184
213
|
## How it's used
|
|
185
214
|
|
|
186
215
|
All six Nous Ergon module repos depend on this lib:
|
|
187
216
|
|
|
188
217
|
| Module | Repo | What it imports from here |
|
|
189
218
|
|---|---|---|
|
|
190
|
-
| Data | [`alpha-engine-data`](https://github.com/cipher813/alpha-engine-data) | `logging`, `preflight`, `arcticdb`, `dates`, `trading_calendar`, `rag` (ingestion) |
|
|
219
|
+
| Data | [`alpha-engine-data`](https://github.com/cipher813/alpha-engine-data) | `logging`, `preflight`, `arcticdb`, `dates`, `trading_calendar`, `rag` (ingestion), `ec2_spot` + `ssm_log_capture` + `ssm_dispatcher` (spot launchers) |
|
|
191
220
|
| Research | [`alpha-engine-research`](https://github.com/cipher813/alpha-engine-research) | `logging`, `decision_capture`, `cost`, `dates`, `rag` (retrieval), `agent_schemas` (canonical LLM-output contracts) |
|
|
192
|
-
| Predictor | [`alpha-engine-predictor`](https://github.com/cipher813/alpha-engine-predictor) | `logging`, `preflight`, `arcticdb`, `dates` |
|
|
221
|
+
| Predictor | [`alpha-engine-predictor`](https://github.com/cipher813/alpha-engine-predictor) | `logging`, `preflight`, `arcticdb`, `dates`, `ec2_spot` + `ssm_log_capture` + `ssm_dispatcher` (spot launcher) |
|
|
193
222
|
| Executor | [`alpha-engine`](https://github.com/cipher813/alpha-engine) | `logging`, `preflight`, `arcticdb`, `dates`, `trading_calendar` |
|
|
194
|
-
| Backtester | [`alpha-engine-backtester`](https://github.com/cipher813/alpha-engine-backtester) | `logging`, `preflight`, `arcticdb`, `dates`, `agent_schemas` (replay-harness Pydantic validation) |
|
|
195
|
-
| Dashboard | [`alpha-engine-dashboard`](https://github.com/cipher813/alpha-engine-dashboard) | `logging`, `arcticdb`, `dates` |
|
|
223
|
+
| Backtester | [`alpha-engine-backtester`](https://github.com/cipher813/alpha-engine-backtester) | `logging`, `preflight`, `arcticdb`, `dates`, `agent_schemas` (replay-harness Pydantic validation), `ec2_spot` + `ssm_log_capture` + `ssm_dispatcher` (spot launcher) |
|
|
224
|
+
| Dashboard | [`alpha-engine-dashboard`](https://github.com/cipher813/alpha-engine-dashboard) | `logging`, `arcticdb`, `dates`, hosts the SSM-target `.venv` that `ssm_dispatcher` invokes via `python -m` |
|
|
196
225
|
|
|
197
226
|
## Development
|
|
198
227
|
|
|
@@ -152,18 +152,47 @@ results = retrieve(
|
|
|
152
152
|
|
|
153
153
|
Requires the `[rag]` extra. Embeddings are Voyage `voyage-3-lite` (512d); the database backend is Neon Postgres with pgvector + HNSW indexes.
|
|
154
154
|
|
|
155
|
+
### `ssm_dispatcher` — SSM send-command + poll chokepoint
|
|
156
|
+
|
|
157
|
+
Canonical Python primitive for the `run_ssm` bash helper that previously appeared as a ~54-line mirror across each dispatcher script that drives a spot instance over the SSM transport. The pre-lift shape — base64-wrap the script body, `aws ssm send-command --document-name AWS-RunShellScript`, loop on `get-command-invocation`, stream the `StandardOutputContent` delta, propagate the inner exit — now lives in one place where the polling cadence, error-class handling, and S3 output-key layout match across every consumer.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python -m alpha_engine_lib.ssm_dispatcher run \
|
|
161
|
+
--instance-id "$INSTANCE_ID" \
|
|
162
|
+
--description "bootstrap" \
|
|
163
|
+
--timeout 3600 \
|
|
164
|
+
--output-bucket "$S3_BUCKET" \
|
|
165
|
+
--output-key-prefix "${S3_STAGING_PREFIX}/ssm-output" \
|
|
166
|
+
--region "$AWS_REGION" \
|
|
167
|
+
--script-stdin <<'BOOTSTRAP'
|
|
168
|
+
set -eo pipefail
|
|
169
|
+
export HOME=/home/ec2-user AWS_REGION=us-east-1
|
|
170
|
+
# ...the script body the SSM target will execute...
|
|
171
|
+
BOOTSTRAP
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Exit 0 on `Success`; exit 1 on any terminal non-Success status, send-command failure, or unrecoverable poll failure; exit 2 on bad CLI input. `InvocationDoesNotExist` during the first 60s after SendCommand counts as a registration race and keeps polling — closes the 2026-05-23 Saturday SF substrate weakness at the chokepoint rather than per-SF-JSON Retry block.
|
|
175
|
+
|
|
176
|
+
### `ssm_log_capture` — SSM-step log capture + S3 ship-on-exit chokepoint
|
|
177
|
+
|
|
178
|
+
Pairs with `ssm_dispatcher` on the SSM target side. The dispatcher script tells the target instance to invoke `python -m alpha_engine_lib.ssm_log_capture run --slug X --log /var/log/X.log -- bash <launcher>`; the target wrapper tees the launcher's stdout/stderr to a local log file and to its own stdout (so the SSM `StandardOutputContent` channel still surfaces output to the dispatcher), then on exit ships the full local log to `s3://alpha-engine-research/_ssm_logs/{slug}/{date}/{host}-{time}.log` regardless of the inner exit code. Replaces the inline `trap 'aws s3 cp ...' EXIT` pattern that broke under ASL `States.Array` escape semantics (2026-05-22 Friday-PM dry-pass catch).
|
|
179
|
+
|
|
180
|
+
### `ec2_spot` — capacity-resilient spot-launch chokepoint
|
|
181
|
+
|
|
182
|
+
Rotates across `(instance_type × subnet)` combinations on `InsufficientInstanceCapacity` / `InsufficientHostCapacity` / `Unsupported` / `InvalidAvailabilityZone` / `SpotMaxPriceTooLow`; non-capacity errors raise immediately. CLI exit 64 distinguishes capacity exhaustion from generic failure. Replaces the hardcoded single-subnet + single-instance-type launch pattern that mirrored across each dispatcher; landed 2026-05-22 after the third-recurrence-in-a-month spot-launch fragility.
|
|
183
|
+
|
|
155
184
|
## How it's used
|
|
156
185
|
|
|
157
186
|
All six Nous Ergon module repos depend on this lib:
|
|
158
187
|
|
|
159
188
|
| Module | Repo | What it imports from here |
|
|
160
189
|
|---|---|---|
|
|
161
|
-
| Data | [`alpha-engine-data`](https://github.com/cipher813/alpha-engine-data) | `logging`, `preflight`, `arcticdb`, `dates`, `trading_calendar`, `rag` (ingestion) |
|
|
190
|
+
| Data | [`alpha-engine-data`](https://github.com/cipher813/alpha-engine-data) | `logging`, `preflight`, `arcticdb`, `dates`, `trading_calendar`, `rag` (ingestion), `ec2_spot` + `ssm_log_capture` + `ssm_dispatcher` (spot launchers) |
|
|
162
191
|
| Research | [`alpha-engine-research`](https://github.com/cipher813/alpha-engine-research) | `logging`, `decision_capture`, `cost`, `dates`, `rag` (retrieval), `agent_schemas` (canonical LLM-output contracts) |
|
|
163
|
-
| Predictor | [`alpha-engine-predictor`](https://github.com/cipher813/alpha-engine-predictor) | `logging`, `preflight`, `arcticdb`, `dates` |
|
|
192
|
+
| Predictor | [`alpha-engine-predictor`](https://github.com/cipher813/alpha-engine-predictor) | `logging`, `preflight`, `arcticdb`, `dates`, `ec2_spot` + `ssm_log_capture` + `ssm_dispatcher` (spot launcher) |
|
|
164
193
|
| Executor | [`alpha-engine`](https://github.com/cipher813/alpha-engine) | `logging`, `preflight`, `arcticdb`, `dates`, `trading_calendar` |
|
|
165
|
-
| Backtester | [`alpha-engine-backtester`](https://github.com/cipher813/alpha-engine-backtester) | `logging`, `preflight`, `arcticdb`, `dates`, `agent_schemas` (replay-harness Pydantic validation) |
|
|
166
|
-
| Dashboard | [`alpha-engine-dashboard`](https://github.com/cipher813/alpha-engine-dashboard) | `logging`, `arcticdb`, `dates` |
|
|
194
|
+
| Backtester | [`alpha-engine-backtester`](https://github.com/cipher813/alpha-engine-backtester) | `logging`, `preflight`, `arcticdb`, `dates`, `agent_schemas` (replay-harness Pydantic validation), `ec2_spot` + `ssm_log_capture` + `ssm_dispatcher` (spot launcher) |
|
|
195
|
+
| Dashboard | [`alpha-engine-dashboard`](https://github.com/cipher813/alpha-engine-dashboard) | `logging`, `arcticdb`, `dates`, hosts the SSM-target `.venv` that `ssm_dispatcher` invokes via `python -m` |
|
|
167
196
|
|
|
168
197
|
## Development
|
|
169
198
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpha-engine-lib"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, and Step-Functions execution-state projection. Full surface documented in README."
|
|
7
|
+
version = "0.35.0"
|
|
8
|
+
description = "Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# EC2 still runs Python 3.9 on the always-on micro instance (boto3 drops
|
|
11
11
|
# 3.9 support 2026-04-29, so upgrade is on the near-term roadmap). All
|
{alpha_engine_lib-0.34.0 → alpha_engine_lib-0.35.0}/src/alpha_engine_lib/pipeline_status/read.py
RENAMED
|
@@ -191,6 +191,30 @@ def _label_for_arn(state_machine_arn: str) -> str:
|
|
|
191
191
|
return PIPELINE_LABELS.get(sm_name, sm_name or "Unknown SF")
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
def _region_from_arn(state_machine_arn: str) -> Optional[str]:
|
|
195
|
+
"""Extract the AWS region from a Step Functions ARN.
|
|
196
|
+
|
|
197
|
+
ARN shape: ``arn:aws:states:<region>:<account>:stateMachine:<name>``.
|
|
198
|
+
Returns the region segment, or None if the ARN doesn't parse — in
|
|
199
|
+
which case the boto3 client falls back to its normal region resolution
|
|
200
|
+
(env vars / config / instance metadata). The lib is permissive on
|
|
201
|
+
malformed input here because the downstream boto3 call will fail
|
|
202
|
+
loud with a typed error that surfaces via ``_raise_for_boto_error``.
|
|
203
|
+
|
|
204
|
+
Why this exists: Step Functions is a regional service and boto3
|
|
205
|
+
raises ``NoRegionError`` if no region is discoverable. Streamlit
|
|
206
|
+
systemd environments on EC2 may not have ``AWS_REGION`` set, but the
|
|
207
|
+
ARN ALWAYS carries the region — extracting it eliminates a class of
|
|
208
|
+
"missing region" failures at the lib chokepoint.
|
|
209
|
+
"""
|
|
210
|
+
if not state_machine_arn or not state_machine_arn.startswith("arn:"):
|
|
211
|
+
return None
|
|
212
|
+
parts = state_machine_arn.split(":")
|
|
213
|
+
if len(parts) < 4 or not parts[3]:
|
|
214
|
+
return None
|
|
215
|
+
return parts[3]
|
|
216
|
+
|
|
217
|
+
|
|
194
218
|
def _failure_cause_from(describe_resp: dict) -> str:
|
|
195
219
|
"""Extract + truncate the failure cause from DescribeExecution response.
|
|
196
220
|
|
|
@@ -430,7 +454,7 @@ def read_pipeline_state(
|
|
|
430
454
|
if client is None: # pragma: no cover — production path
|
|
431
455
|
import boto3
|
|
432
456
|
|
|
433
|
-
client = boto3.client("stepfunctions")
|
|
457
|
+
client = boto3.client("stepfunctions", region_name=_region_from_arn(state_machine_arn))
|
|
434
458
|
|
|
435
459
|
label = _label_for_arn(state_machine_arn)
|
|
436
460
|
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSM send-command + poll-for-completion chokepoint.
|
|
3
|
+
|
|
4
|
+
Consolidation substrate for the ``run_ssm`` bash helper that previously
|
|
5
|
+
appeared as a ~54-line mirror in every dispatcher script that drives a
|
|
6
|
+
spot instance over the SSM transport. The first occurrence shipped in
|
|
7
|
+
alpha-engine-predictor #168 (2026-05-15) as part of the SSH/SCP→SSM
|
|
8
|
+
migration; the second and third occurrences land when alpha-engine-data's
|
|
9
|
+
``spot_data_weekly.sh`` and alpha-engine-backtester's ``spot_backtest.sh``
|
|
10
|
+
migrate off SSH+SCP onto the same SSM transport. Per
|
|
11
|
+
``~/Development/CLAUDE.md`` SOTA sub-sub-rule + the
|
|
12
|
+
``[[feedback_lift_invariants_to_chokepoint_after_second_recurrence]]``
|
|
13
|
+
discipline, the pattern lifts to lib at the second recurrence.
|
|
14
|
+
|
|
15
|
+
The pre-lift bash shape was::
|
|
16
|
+
|
|
17
|
+
run_ssm "<description>" "<bash script>" [timeout_seconds]
|
|
18
|
+
# 1. base64-encode the script body (transport-safe wrapping of inner
|
|
19
|
+
# heredocs / quoting)
|
|
20
|
+
# 2. aws ssm send-command --document-name AWS-RunShellScript \
|
|
21
|
+
# --instance-ids "$INSTANCE_ID" \
|
|
22
|
+
# --output-s3-bucket-name "$S3_BUCKET" \
|
|
23
|
+
# --output-s3-key-prefix "${S3_STAGING_PREFIX}/ssm-output" \
|
|
24
|
+
# --timeout-seconds "$timeout_s" \
|
|
25
|
+
# --parameters file://$pfile
|
|
26
|
+
# 3. while :; do
|
|
27
|
+
# aws ssm get-command-invocation --command-id $cmd_id
|
|
28
|
+
# stream stdout delta; check Status; break on terminal
|
|
29
|
+
# done
|
|
30
|
+
# 4. on Success → return 0
|
|
31
|
+
# 5. on Failed/TimedOut/Cancelled → fetch stderr, print, return 1
|
|
32
|
+
|
|
33
|
+
The Python primitive in this module exposes the same contract — base64
|
|
34
|
+
wrap, send, poll, stream, propagate exit — but lives in one place so
|
|
35
|
+
the polling cadence, error-class handling, and S3 output-key layout
|
|
36
|
+
match across every consumer.
|
|
37
|
+
|
|
38
|
+
**Why a CLI, not a bash function:**
|
|
39
|
+
|
|
40
|
+
Per the SOTA / institutional-approach sub-sub-rule ("when mirroring a
|
|
41
|
+
pattern across repos, consider lifting it into ``alpha-engine-lib``...
|
|
42
|
+
Pure-Bash primitives can stay mirrored unless re-expressible as a
|
|
43
|
+
Python CLI entry callable from Bash, in which case the CLI re-expression
|
|
44
|
+
is the institutional path"). The dispatcher script invokes::
|
|
45
|
+
|
|
46
|
+
python -m alpha_engine_lib.ssm_dispatcher run \\
|
|
47
|
+
--instance-id "$INSTANCE_ID" \\
|
|
48
|
+
--description "bootstrap" \\
|
|
49
|
+
--timeout 3600 \\
|
|
50
|
+
--output-bucket "$S3_BUCKET" \\
|
|
51
|
+
--output-key-prefix "${S3_STAGING_PREFIX}/ssm-output" \\
|
|
52
|
+
--region "$AWS_REGION" \\
|
|
53
|
+
--script-stdin <<'BOOTSTRAP'
|
|
54
|
+
set -eo pipefail
|
|
55
|
+
...
|
|
56
|
+
BOOTSTRAP
|
|
57
|
+
|
|
58
|
+
Exit code 0 on Success; 1 on terminal non-Success; 2 on bad input. The
|
|
59
|
+
inner script's stdout streams to the dispatcher's stdout as it arrives
|
|
60
|
+
(SSM ``StandardOutputContent`` delta); on terminal non-Success the
|
|
61
|
+
``StandardErrorContent`` is fetched + printed before the dispatcher
|
|
62
|
+
exits.
|
|
63
|
+
|
|
64
|
+
**InvocationDoesNotExist race:**
|
|
65
|
+
|
|
66
|
+
After ``send-command`` returns a ``CommandId``, the first poll of
|
|
67
|
+
``get-command-invocation`` can race the SSM control plane's registration
|
|
68
|
+
and return ``InvocationDoesNotExist``. The 2026-05-23 Saturday SF showed
|
|
69
|
+
this exact failure mode at event 16 (MorningEnrich first poll), absorbed
|
|
70
|
+
by the SF Catch but representing a substrate weakness. This module
|
|
71
|
+
treats ``InvocationDoesNotExist`` as a transient "Pending" status for
|
|
72
|
+
the first ~60s after SendCommand (the registration window) and as a
|
|
73
|
+
terminal failure thereafter. Mirrors the bash predecessor's
|
|
74
|
+
``2>/dev/null || echo Pending`` swallow without the all-errors-look-like-Pending
|
|
75
|
+
ambiguity.
|
|
76
|
+
|
|
77
|
+
**Failure behavior — never raises:**
|
|
78
|
+
|
|
79
|
+
- Inner command's terminal status maps to exit code 0 (Success) or 1
|
|
80
|
+
(Failed / TimedOut / Cancelled / TerminalError). The dispatcher
|
|
81
|
+
script's ``set -e`` then propagates that exit upward to the SF Catch.
|
|
82
|
+
- Subprocess setup failure (boto3 missing, IAM denied at SendCommand
|
|
83
|
+
time, instance not registered) is logged + returns 1. The caller
|
|
84
|
+
reads the failure from CloudWatch / SSM history; this module's job
|
|
85
|
+
is to be a thin transport, not a recovery layer.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
from __future__ import annotations
|
|
89
|
+
|
|
90
|
+
import argparse
|
|
91
|
+
import base64
|
|
92
|
+
import logging
|
|
93
|
+
import os
|
|
94
|
+
import sys
|
|
95
|
+
import time
|
|
96
|
+
from typing import Final, Optional
|
|
97
|
+
|
|
98
|
+
logger = logging.getLogger(__name__)
|
|
99
|
+
|
|
100
|
+
# Status taxonomy from SSM's get-command-invocation. Terminal non-Success
|
|
101
|
+
# statuses all map to exit 1.
|
|
102
|
+
TERMINAL_NON_SUCCESS: Final[frozenset[str]] = frozenset(
|
|
103
|
+
{"Cancelled", "Failed", "TimedOut", "Cancelling", "TerminalError"}
|
|
104
|
+
)
|
|
105
|
+
PENDING_STATUSES: Final[frozenset[str]] = frozenset(
|
|
106
|
+
{"Pending", "InProgress", "Delayed"}
|
|
107
|
+
)
|
|
108
|
+
SUCCESS_STATUS: Final[str] = "Success"
|
|
109
|
+
|
|
110
|
+
# Window during which InvocationDoesNotExist counts as a registration race
|
|
111
|
+
# rather than a true failure. Mirrors the empirical observation that the
|
|
112
|
+
# SSM control plane has settled by ~30s post-SendCommand under normal
|
|
113
|
+
# conditions; 60s is a defensive ceiling.
|
|
114
|
+
REGISTRATION_GRACE_SECONDS: Final[int] = 60
|
|
115
|
+
|
|
116
|
+
# Poll cadence — matches the bash predecessor's `sleep 5`.
|
|
117
|
+
DEFAULT_POLL_INTERVAL_SECONDS: Final[float] = 5.0
|
|
118
|
+
|
|
119
|
+
# StandardOutputContent / StandardErrorContent fields are capped at 24KB
|
|
120
|
+
# in get-command-invocation responses. Beyond the cap the buffer rotates
|
|
121
|
+
# (we detect by a length decrease) and the full log lives in the
|
|
122
|
+
# configured S3 output prefix.
|
|
123
|
+
SSM_INLINE_OUTPUT_CAP_BYTES: Final[int] = 24 * 1024
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SsmDispatchError(Exception):
|
|
127
|
+
"""Non-recoverable SSM send-command / poll failure."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _encode_command_payload(script: str) -> str:
|
|
131
|
+
"""Wrap ``script`` for AWS-RunShellScript transport.
|
|
132
|
+
|
|
133
|
+
The pre-lift bash helper base64-encoded the script body and emitted
|
|
134
|
+
a single command ``echo <b64> | base64 -d | bash``. This is the
|
|
135
|
+
transport-safe wrapping that lets the script contain heredocs,
|
|
136
|
+
embedded Python, single quotes, etc. without ASL/SSM escaping
|
|
137
|
+
surface.
|
|
138
|
+
"""
|
|
139
|
+
b64 = base64.b64encode(script.encode("utf-8")).decode("ascii")
|
|
140
|
+
return f"echo {b64} | base64 -d | bash"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run(
|
|
144
|
+
instance_id: str,
|
|
145
|
+
description: str,
|
|
146
|
+
script: str,
|
|
147
|
+
*,
|
|
148
|
+
timeout_seconds: int = 3600,
|
|
149
|
+
output_bucket: Optional[str] = None,
|
|
150
|
+
output_key_prefix: Optional[str] = None,
|
|
151
|
+
region: str = "us-east-1",
|
|
152
|
+
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
|
|
153
|
+
stdout_stream=None,
|
|
154
|
+
stderr_stream=None,
|
|
155
|
+
sleep=time.sleep,
|
|
156
|
+
monotonic=time.monotonic,
|
|
157
|
+
boto3_client=None,
|
|
158
|
+
) -> int:
|
|
159
|
+
"""Send ``script`` to ``instance_id`` via SSM, poll until terminal, stream stdout.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
instance_id: target EC2 instance ID (must be SSM-registered).
|
|
163
|
+
description: short label for SSM history + dispatcher logs.
|
|
164
|
+
script: bash script body. Will be base64-wrapped + executed as
|
|
165
|
+
a single AWS-RunShellScript command.
|
|
166
|
+
timeout_seconds: SSM command timeout (handed to SendCommand).
|
|
167
|
+
output_bucket: S3 bucket for SSM to write the full stdout/stderr
|
|
168
|
+
(past the 24KB inline cap). Optional; if unset, only inline
|
|
169
|
+
output is available.
|
|
170
|
+
output_key_prefix: S3 key prefix for the SSM output bucket.
|
|
171
|
+
region: AWS region.
|
|
172
|
+
poll_interval_seconds: gap between get-command-invocation polls.
|
|
173
|
+
stdout_stream: destination for streamed inner stdout (default:
|
|
174
|
+
``sys.stdout``).
|
|
175
|
+
stderr_stream: destination for the terminal-failure stderr dump
|
|
176
|
+
(default: ``sys.stderr``).
|
|
177
|
+
sleep / monotonic: time hooks (overridable for tests).
|
|
178
|
+
boto3_client: optional boto3 ``ssm`` client (for tests). When
|
|
179
|
+
``None``, constructed via ``boto3.client('ssm', region_name=region)``.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
``0`` on terminal Success.
|
|
183
|
+
``1`` on any terminal non-Success status, send-command failure,
|
|
184
|
+
or unrecoverable poll failure.
|
|
185
|
+
|
|
186
|
+
Never raises.
|
|
187
|
+
"""
|
|
188
|
+
out = stdout_stream if stdout_stream is not None else sys.stdout
|
|
189
|
+
err = stderr_stream if stderr_stream is not None else sys.stderr
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
if boto3_client is None:
|
|
193
|
+
import boto3
|
|
194
|
+
|
|
195
|
+
ssm = boto3.client("ssm", region_name=region)
|
|
196
|
+
else:
|
|
197
|
+
ssm = boto3_client
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
print(
|
|
200
|
+
f"ssm_dispatcher: boto3 client construction failed: "
|
|
201
|
+
f"{type(exc).__name__}: {exc}",
|
|
202
|
+
file=err,
|
|
203
|
+
)
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
payload = _encode_command_payload(script)
|
|
207
|
+
send_kwargs: dict = {
|
|
208
|
+
"InstanceIds": [instance_id],
|
|
209
|
+
"DocumentName": "AWS-RunShellScript",
|
|
210
|
+
"Comment": description[:100], # SSM Comment cap is 100 chars
|
|
211
|
+
"TimeoutSeconds": int(timeout_seconds),
|
|
212
|
+
"Parameters": {"commands": [payload]},
|
|
213
|
+
}
|
|
214
|
+
if output_bucket:
|
|
215
|
+
send_kwargs["OutputS3BucketName"] = output_bucket
|
|
216
|
+
if output_key_prefix:
|
|
217
|
+
send_kwargs["OutputS3KeyPrefix"] = output_key_prefix
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
resp = ssm.send_command(**send_kwargs)
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
print(
|
|
223
|
+
f"ssm_dispatcher: send_command failed for {description!r}: "
|
|
224
|
+
f"{type(exc).__name__}: {exc}",
|
|
225
|
+
file=err,
|
|
226
|
+
)
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
command_id = resp.get("Command", {}).get("CommandId")
|
|
230
|
+
if not command_id:
|
|
231
|
+
print(
|
|
232
|
+
f"ssm_dispatcher: send_command returned no CommandId for {description!r}",
|
|
233
|
+
file=err,
|
|
234
|
+
)
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
print(f" [ssm {description}] command-id={command_id}", file=err)
|
|
238
|
+
|
|
239
|
+
start_monotonic = monotonic()
|
|
240
|
+
last_out_len = 0
|
|
241
|
+
|
|
242
|
+
while True:
|
|
243
|
+
sleep(poll_interval_seconds)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
inv = ssm.get_command_invocation(
|
|
247
|
+
CommandId=command_id,
|
|
248
|
+
InstanceId=instance_id,
|
|
249
|
+
)
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
code = _classify_boto_exception(exc)
|
|
252
|
+
if code == "InvocationDoesNotExist":
|
|
253
|
+
elapsed = monotonic() - start_monotonic
|
|
254
|
+
if elapsed <= REGISTRATION_GRACE_SECONDS:
|
|
255
|
+
# Registration race per the 2026-05-23 Saturday SF
|
|
256
|
+
# event-16 substrate weakness; keep polling.
|
|
257
|
+
continue
|
|
258
|
+
print(
|
|
259
|
+
f"ssm_dispatcher: {description!r} command {command_id} "
|
|
260
|
+
f"never registered (InvocationDoesNotExist after "
|
|
261
|
+
f"{elapsed:.0f}s)",
|
|
262
|
+
file=err,
|
|
263
|
+
)
|
|
264
|
+
return 1
|
|
265
|
+
# Other transient classes that the bash predecessor swallowed
|
|
266
|
+
# via `2>/dev/null || echo Pending`. Be explicit: only the
|
|
267
|
+
# listed set is treated as transient; anything else is a hard
|
|
268
|
+
# failure.
|
|
269
|
+
if code in {"ThrottlingException", "RequestLimitExceeded"}:
|
|
270
|
+
continue
|
|
271
|
+
print(
|
|
272
|
+
f"ssm_dispatcher: get_command_invocation for {description!r} "
|
|
273
|
+
f"raised {code}: {exc}",
|
|
274
|
+
file=err,
|
|
275
|
+
)
|
|
276
|
+
return 1
|
|
277
|
+
|
|
278
|
+
status = inv.get("Status", "Pending")
|
|
279
|
+
std_out = inv.get("StandardOutputContent", "") or ""
|
|
280
|
+
|
|
281
|
+
if len(std_out) > last_out_len:
|
|
282
|
+
out.write(std_out[last_out_len:])
|
|
283
|
+
out.flush()
|
|
284
|
+
last_out_len = len(std_out)
|
|
285
|
+
elif len(std_out) < last_out_len:
|
|
286
|
+
# 24KB cap rotated the buffer; the full log is in S3 (if
|
|
287
|
+
# output_bucket was configured).
|
|
288
|
+
cap_note = (
|
|
289
|
+
f" [ssm {description}] (stdout exceeded "
|
|
290
|
+
f"{SSM_INLINE_OUTPUT_CAP_BYTES // 1024}KB cap — full log: "
|
|
291
|
+
f"s3://{output_bucket}/{output_key_prefix}/)\n"
|
|
292
|
+
if output_bucket
|
|
293
|
+
else (
|
|
294
|
+
f" [ssm {description}] (stdout exceeded "
|
|
295
|
+
f"{SSM_INLINE_OUTPUT_CAP_BYTES // 1024}KB cap — "
|
|
296
|
+
"configure --output-bucket for full log)\n"
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
err.write(cap_note)
|
|
300
|
+
err.flush()
|
|
301
|
+
last_out_len = len(std_out)
|
|
302
|
+
|
|
303
|
+
if status == SUCCESS_STATUS:
|
|
304
|
+
return 0
|
|
305
|
+
if status in TERMINAL_NON_SUCCESS:
|
|
306
|
+
std_err = inv.get("StandardErrorContent", "") or ""
|
|
307
|
+
err.write(
|
|
308
|
+
f"ERROR: SSM step {description!r} terminal status={status}\n"
|
|
309
|
+
)
|
|
310
|
+
if std_err:
|
|
311
|
+
err.write(
|
|
312
|
+
f"--- stderr ({SSM_INLINE_OUTPUT_CAP_BYTES // 1024}KB cap; "
|
|
313
|
+
)
|
|
314
|
+
if output_bucket:
|
|
315
|
+
err.write(
|
|
316
|
+
f"full: s3://{output_bucket}/{output_key_prefix}/) ---\n"
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
err.write("configure --output-bucket for full log) ---\n")
|
|
320
|
+
err.write(std_err)
|
|
321
|
+
if not std_err.endswith("\n"):
|
|
322
|
+
err.write("\n")
|
|
323
|
+
err.flush()
|
|
324
|
+
return 1
|
|
325
|
+
if status not in PENDING_STATUSES:
|
|
326
|
+
# Unknown status — treat as a hard failure, log it.
|
|
327
|
+
err.write(
|
|
328
|
+
f"ssm_dispatcher: {description!r} returned unknown status "
|
|
329
|
+
f"{status!r}; treating as failure\n"
|
|
330
|
+
)
|
|
331
|
+
err.flush()
|
|
332
|
+
return 1
|
|
333
|
+
# Pending / InProgress / Delayed — keep polling.
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _classify_boto_exception(exc: BaseException) -> str:
|
|
337
|
+
"""Extract the ``Error.Code`` from a botocore ClientError.
|
|
338
|
+
|
|
339
|
+
Returns the exception class name when no ``response.Error.Code`` is
|
|
340
|
+
available (e.g., on non-botocore exceptions). Tests patch this for
|
|
341
|
+
deterministic InvocationDoesNotExist surfacing.
|
|
342
|
+
"""
|
|
343
|
+
response = getattr(exc, "response", None)
|
|
344
|
+
if isinstance(response, dict):
|
|
345
|
+
code = response.get("Error", {}).get("Code")
|
|
346
|
+
if code:
|
|
347
|
+
return str(code)
|
|
348
|
+
return type(exc).__name__
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _read_script(args: argparse.Namespace) -> str:
|
|
352
|
+
if args.script_file:
|
|
353
|
+
with open(args.script_file, "r", encoding="utf-8") as fh:
|
|
354
|
+
return fh.read()
|
|
355
|
+
if args.script_stdin:
|
|
356
|
+
return sys.stdin.read()
|
|
357
|
+
raise SystemExit(
|
|
358
|
+
"ssm_dispatcher: must pass either --script-file PATH or --script-stdin "
|
|
359
|
+
"(with the script body on stdin)"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main(argv: list[str] | None = None) -> int:
|
|
364
|
+
parser = argparse.ArgumentParser(
|
|
365
|
+
prog="python -m alpha_engine_lib.ssm_dispatcher",
|
|
366
|
+
description=(
|
|
367
|
+
"Send a bash script to an SSM-registered EC2 instance via "
|
|
368
|
+
"AWS-RunShellScript, poll until terminal, stream stdout to "
|
|
369
|
+
"this process, and propagate the inner exit status. The "
|
|
370
|
+
"institutional replacement for the ~54-line run_ssm bash "
|
|
371
|
+
"helper mirrored across alpha-engine-* dispatcher scripts."
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
subparsers = parser.add_subparsers(dest="cmd", required=True)
|
|
375
|
+
|
|
376
|
+
run_p = subparsers.add_parser(
|
|
377
|
+
"run",
|
|
378
|
+
help="Dispatch a script to an instance and stream its output.",
|
|
379
|
+
)
|
|
380
|
+
run_p.add_argument(
|
|
381
|
+
"--instance-id",
|
|
382
|
+
required=True,
|
|
383
|
+
help="Target EC2 instance ID (must be SSM-registered).",
|
|
384
|
+
)
|
|
385
|
+
run_p.add_argument(
|
|
386
|
+
"--description",
|
|
387
|
+
required=True,
|
|
388
|
+
help=(
|
|
389
|
+
"Short label for the SSM command Comment + dispatcher log "
|
|
390
|
+
"lines (e.g., 'bootstrap', 'full-training')."
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
run_p.add_argument(
|
|
394
|
+
"--timeout",
|
|
395
|
+
type=int,
|
|
396
|
+
default=3600,
|
|
397
|
+
help="SSM command timeout in seconds (default: 3600).",
|
|
398
|
+
)
|
|
399
|
+
run_p.add_argument(
|
|
400
|
+
"--output-bucket",
|
|
401
|
+
default=None,
|
|
402
|
+
help=(
|
|
403
|
+
"S3 bucket where SSM writes the full stdout/stderr beyond "
|
|
404
|
+
"the inline 24KB cap. Optional; without it, only the inline "
|
|
405
|
+
"delta is available."
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
run_p.add_argument(
|
|
409
|
+
"--output-key-prefix",
|
|
410
|
+
default=None,
|
|
411
|
+
help="S3 key prefix under --output-bucket for the SSM output.",
|
|
412
|
+
)
|
|
413
|
+
run_p.add_argument(
|
|
414
|
+
"--region",
|
|
415
|
+
default=os.environ.get("AWS_REGION", "us-east-1"),
|
|
416
|
+
help="AWS region (default: $AWS_REGION or us-east-1).",
|
|
417
|
+
)
|
|
418
|
+
run_p.add_argument(
|
|
419
|
+
"--poll-interval",
|
|
420
|
+
type=float,
|
|
421
|
+
default=DEFAULT_POLL_INTERVAL_SECONDS,
|
|
422
|
+
help=(
|
|
423
|
+
"Seconds between get-command-invocation polls (default: "
|
|
424
|
+
f"{DEFAULT_POLL_INTERVAL_SECONDS:g})."
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
script_grp = run_p.add_mutually_exclusive_group(required=True)
|
|
428
|
+
script_grp.add_argument(
|
|
429
|
+
"--script-file",
|
|
430
|
+
default=None,
|
|
431
|
+
help="Path to a local file containing the bash script body.",
|
|
432
|
+
)
|
|
433
|
+
script_grp.add_argument(
|
|
434
|
+
"--script-stdin",
|
|
435
|
+
action="store_true",
|
|
436
|
+
help="Read the bash script body from stdin (heredoc-friendly).",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
args = parser.parse_args(argv)
|
|
440
|
+
logging.basicConfig(level=logging.WARNING)
|
|
441
|
+
|
|
442
|
+
script = _read_script(args)
|
|
443
|
+
if not script.strip():
|
|
444
|
+
print(
|
|
445
|
+
"ssm_dispatcher: empty script body (refusing to dispatch a no-op)",
|
|
446
|
+
file=sys.stderr,
|
|
447
|
+
)
|
|
448
|
+
return 2
|
|
449
|
+
|
|
450
|
+
return run(
|
|
451
|
+
instance_id=args.instance_id,
|
|
452
|
+
description=args.description,
|
|
453
|
+
script=script,
|
|
454
|
+
timeout_seconds=args.timeout,
|
|
455
|
+
output_bucket=args.output_bucket,
|
|
456
|
+
output_key_prefix=args.output_key_prefix,
|
|
457
|
+
region=args.region,
|
|
458
|
+
poll_interval_seconds=args.poll_interval,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
if __name__ == "__main__":
|
|
463
|
+
sys.exit(main())
|