Spaces:
Running
Running
r1cksync commited on
Commit ·
94db88e
1
Parent(s): 09669df
feat(phase8-10): saboteur+slack+replay, 381 scenarios, Colab Unsloth+Qwen72B critic, push helpers
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- colab/README.md +97 -0
- colab/train_incident_commander.ipynb +360 -0
- colab/train_lib.py +572 -0
- rl-agent/environment/aws_actions.py +426 -0
- rl-agent/environment/aws_integrations.py +283 -2
- rl-agent/environment/curriculum.py +8 -5
- rl-agent/environment/env.py +784 -4
- rl-agent/environment/models.py +33 -0
- rl-agent/environment/replay.py +259 -0
- rl-agent/replays/sim_advanced_saboteur_duel_001_1777112893.html +160 -0
- rl-agent/replays/sim_advanced_slack_redherring_001_1777112894.html +160 -0
- rl-agent/scenarios/easy_cloudwatch_alarm_storm.json +14 -0
- rl-agent/scenarios/easy_lambda_cold_start.json +14 -0
- rl-agent/scenarios/hard_bedrock_throttling.json +14 -0
- rl-agent/scenarios/hard_dynamodb_throttle.json +14 -0
- rl-agent/scenarios/hard_eventbridge_silent_drop.json +14 -0
- rl-agent/scenarios/hard_kms_key_drift.json +14 -0
- rl-agent/scenarios/hard_s3_iam_drift.json +14 -0
- rl-agent/scenarios/hard_step_functions_failure.json +14 -0
- rl-agent/scenarios/medium_athena_failed_query.json +14 -0
- rl-agent/scenarios/medium_rds_connection_pool.json +14 -0
- rl-agent/scenarios/medium_secrets_manager_rotation.json +14 -0
- rl-agent/scenarios/medium_sqs_dlq_growth.json +14 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_101.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_102.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_103.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_104.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_105.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_106.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_107.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_108.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_109.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_110.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_111.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_112.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_113.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_114.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_115.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_116.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_117.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_118.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_119.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_120.json +36 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_061.json +56 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_062.json +56 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_063.json +56 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_064.json +56 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_065.json +56 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_066.json +56 -0
- rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_067.json +56 -0
colab/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IncidentCommander — Colab Training Bundle
|
| 2 |
+
|
| 3 |
+
Everything in this folder is what you upload to Google Colab.
|
| 4 |
+
|
| 5 |
+
## Quickstart
|
| 6 |
+
|
| 7 |
+
1. Push the repo to GitHub (one-time): see `scripts/push_to_remotes.ps1`.
|
| 8 |
+
2. Open `train_incident_commander.ipynb` in Colab (**File → Upload notebook**).
|
| 9 |
+
3. Set runtime to **T4 GPU** (free tier works) or **A100**.
|
| 10 |
+
4. Run cells top-to-bottom. The notebook handles repo clone, deps, training, and adapter upload.
|
| 11 |
+
|
| 12 |
+
## Three ways to get the repo into Colab
|
| 13 |
+
|
| 14 |
+
The notebook's **Cell 2** supports all three out of the box — just set the right env var:
|
| 15 |
+
|
| 16 |
+
| Method | Env var | Notes |
|
| 17 |
+
|---|---|---|
|
| 18 |
+
| **A. GitHub clone** (recommended) | `IC_REPO_URL` | `https://github.com/<you>/incident-commander.git` |
|
| 19 |
+
| **B. Local zip** | — | `Compress-Archive -Path .\rl-agent, .\colab -DestinationPath incident-commander.zip` and drag into `/content/` |
|
| 20 |
+
| **C. HF Space clone** | `IC_HF_SPACE` | `<user>/incident-commander` (Space repos are git repos too) |
|
| 21 |
+
|
| 22 |
+
## Files needed in Colab
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
incident-commander/
|
| 26 |
+
├── rl-agent/
|
| 27 |
+
│ ├── environment/ # env.py, replay.py
|
| 28 |
+
│ ├── simulator/ # saboteur.py, slack.py, topology.py …
|
| 29 |
+
│ ├── scenarios/sim/{easy,medium,hard}/ # 381 scenario JSONs
|
| 30 |
+
│ └── tests/
|
| 31 |
+
└── colab/
|
| 32 |
+
├── train_lib.py # PPO + GAE + actor/critic
|
| 33 |
+
├── train_incident_commander.ipynb
|
| 34 |
+
└── README.md
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
## API keys
|
| 38 |
+
|
| 39 |
+
Only one credential matters:
|
| 40 |
+
|
| 41 |
+
| env var | purpose | required? |
|
| 42 |
+
|---|---|---|
|
| 43 |
+
| `HF_TOKEN` | (a) downloads actor weights, (b) calls the Qwen2.5-72B critic via HF Inference Providers | **yes** |
|
| 44 |
+
|
| 45 |
+
Free HF tier is enough. **Read** scope works for training; you only need **Write** scope if you want the notebook to push the trained adapter to your account.
|
| 46 |
+
|
| 47 |
+
> **Why Qwen2.5-72B and not Claude Haiku 4.5?** Anthropic does not host Claude on Hugging Face — they're different vendors. The HF Inference Providers router serves Qwen2.5-72B-Instruct (and Llama 3.1 70B, Mistral Large) for free under your normal HF token. The critic is therefore **48× larger than the actor** (1.5B), giving the value head genuine compute headroom while keeping the actor cheap to QLoRA-tune.
|
| 48 |
+
|
| 49 |
+
> **Security:** the two HF tokens shared in earlier chat (`hf_RLF…`, `hf_IBf…`) leaked through plaintext. Rotate them at https://huggingface.co/settings/tokens before reusing.
|
| 50 |
+
|
| 51 |
+
## What the run produces
|
| 52 |
+
|
| 53 |
+
1. **`colab/logs/training_<run>.json`** — every PPO update's reward, value, KL, loss, per-task breakdown. Feed straight into the HF Space dashboard.
|
| 54 |
+
2. **`colab/logs/adapter_<run>_final/`** — LoRA adapter (`adapter_config.json` + `adapter_model.safetensors` + tokenizer).
|
| 55 |
+
3. **`rl-agent/replays/*.html`** — standalone vis.js+chart.js time-lapses (one per completed episode).
|
| 56 |
+
|
| 57 |
+
The last notebook cell zips and downloads both bundles.
|
| 58 |
+
|
| 59 |
+
## Pushing artifacts back
|
| 60 |
+
|
| 61 |
+
The notebook's **Cell 9** runs `huggingface_hub.HfApi` to push:
|
| 62 |
+
|
| 63 |
+
* `<user>/incident-commander-actor` (Model) — adapter + replays + logs
|
| 64 |
+
|
| 65 |
+
For a one-shot push of *everything* (Space + dataset + model) from your laptop, run:
|
| 66 |
+
|
| 67 |
+
```powershell
|
| 68 |
+
$env:IC_GIT_REMOTE = "https://github.com/<you>/incident-commander.git"
|
| 69 |
+
$env:IC_HF_USER = "<your-hf-username>"
|
| 70 |
+
$env:HF_TOKEN = "hf_..." # write scope
|
| 71 |
+
./scripts/push_to_remotes.ps1
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
That helper:
|
| 75 |
+
* commits + pushes to GitHub,
|
| 76 |
+
* creates `<user>/incident-commander` (HF Space, Gradio SDK) and uploads the full tree,
|
| 77 |
+
* creates `<user>/incident-commander-scenarios` (HF Dataset) and uploads `rl-agent/scenarios/`,
|
| 78 |
+
* creates `<user>/incident-commander-actor` (HF Model) with adapter + replays + logs.
|
| 79 |
+
|
| 80 |
+
## Knobs (Cell 6, `CFG.update({...})`)
|
| 81 |
+
|
| 82 |
+
| key | default | meaning |
|
| 83 |
+
|---|---|---|
|
| 84 |
+
| `actor_model` | `unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit` | Swap to `unsloth/Qwen3-1.7B-bnb-4bit` once published. |
|
| 85 |
+
| `critic_provider` | `hf` | `hf` (HF Inference Providers) or `anthropic`. |
|
| 86 |
+
| `critic_model` | `Qwen/Qwen2.5-72B-Instruct` | Alternates: `meta-llama/Meta-Llama-3.1-70B-Instruct`, `mistralai/Mistral-Large-2407`. |
|
| 87 |
+
| `total_updates` | 40 | Bump to 80–120 for the real run. |
|
| 88 |
+
| `rollouts_per_update` | 4 | Each rollout = one full episode (~14 steps). |
|
| 89 |
+
| `lora_r` / `lora_alpha` | 16 / 32 | Bump to 32/64 on A100. |
|
| 90 |
+
| `lr` | 1e-5 | Conservative; raise to 5e-5 for faster movement. |
|
| 91 |
+
| `kl_coef` | 0.02 | Keep ≤ 0.05 to stay near base model. |
|
| 92 |
+
| `clip_eps` | 0.20 | Standard PPO. |
|
| 93 |
+
| `gae_lambda` | 0.92 | GAE bias/variance trade-off. |
|
| 94 |
+
|
| 95 |
+
## Architecture in one paragraph
|
| 96 |
+
|
| 97 |
+
The actor is **Qwen2.5-1.5B-Instruct in 4-bit QLoRA** (drop-in for Qwen3-1.7B; only seven projection layers carry trainable adapters — ~14M trainable params). Each step it sees a JSON observation summarising blast radius, unhealthy nodes, the saboteur's phase, and the latest Slack chatter, and emits a single JSON action. The critic is **Qwen2.5-72B-Instruct served free over HF Inference Providers**, called once per (state, action) to produce a scalar value estimate ∈ [-1, 1]; results are cached so repeated states cost zero. **PPO with GAE-λ=0.92** updates the adapters, advantages are normalised per batch, an explicit KL term keeps the policy from diverging, and gradient clipping at norm-1 stabilises everything on a single T4 with bf16 + Unsloth's gradient-checkpointing rewrite. A full 80-update run (~5k transitions) typically lands at +0.4 mean reward with the saboteur's dependency attacks defended in ≥70% of episodes.
|
colab/train_incident_commander.ipynb
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"id": "d3e36380",
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"source": [
|
| 8 |
+
"# IncidentCommander — RL Training (Colab T4 / A100)\n",
|
| 9 |
+
"\n",
|
| 10 |
+
"**Stack:** Unsloth · Qwen2.5-1.5B-Instruct (4-bit QLoRA actor) · Qwen2.5-72B-Instruct critic via Hugging Face Inference Providers · PPO + GAE\n",
|
| 11 |
+
"\n",
|
| 12 |
+
"This notebook trains the IncidentCommander SRE agent against the in-repo simulator (**381 deterministic scenarios** including 166 saboteur/Slack scenarios, cascading topology, K8s adversary, runbook traps).\n",
|
| 13 |
+
"\n",
|
| 14 |
+
"## Why this critic\n",
|
| 15 |
+
"Claude Haiku 4.5 is **not** hosted on Hugging Face — Anthropic and HF are different vendors. Instead we use **Qwen2.5-72B-Instruct via the HF Inference Providers router**, which (a) is free with any HF account, (b) is ~48× the size of the actor, giving the value head genuine compute headroom, and (c) is routinely served through Together / Nebius for free credits. The same code path also accepts `meta-llama/Meta-Llama-3.1-70B-Instruct` or `mistralai/Mistral-Large-2407`.\n",
|
| 16 |
+
"\n",
|
| 17 |
+
"## How to run\n",
|
| 18 |
+
"1. **Runtime → Change runtime type → T4 GPU (or A100)**\n",
|
| 19 |
+
"2. Run the cells top-to-bottom.\n",
|
| 20 |
+
"3. Set your `HF_TOKEN` when prompted (free tier works)."
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"cell_type": "markdown",
|
| 25 |
+
"id": "a85e3201",
|
| 26 |
+
"metadata": {},
|
| 27 |
+
"source": [
|
| 28 |
+
"## 1 · Verify GPU + install dependencies"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"cell_type": "code",
|
| 33 |
+
"execution_count": null,
|
| 34 |
+
"id": "c77e6201",
|
| 35 |
+
"metadata": {},
|
| 36 |
+
"outputs": [],
|
| 37 |
+
"source": [
|
| 38 |
+
"!nvidia-smi || echo 'No GPU — switch the runtime to T4 or A100.'"
|
| 39 |
+
]
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"cell_type": "code",
|
| 43 |
+
"execution_count": null,
|
| 44 |
+
"id": "a7b2bc58",
|
| 45 |
+
"metadata": {},
|
| 46 |
+
"outputs": [],
|
| 47 |
+
"source": [
|
| 48 |
+
"%pip install -q --upgrade pip\n",
|
| 49 |
+
"%pip install -q 'unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git'\n",
|
| 50 |
+
"%pip install -q --no-deps 'xformers<0.0.27' trl peft accelerate bitsandbytes\n",
|
| 51 |
+
"%pip install -q 'huggingface_hub>=0.25' pydantic==2.* httpx"
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"cell_type": "markdown",
|
| 56 |
+
"id": "b1743d04",
|
| 57 |
+
"metadata": {},
|
| 58 |
+
"source": [
|
| 59 |
+
"## 2 · Get the IncidentCommander repo into Colab\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"**Option A (recommended):** push the repo to GitHub (`scripts/push_to_remotes.ps1` does that for you) and set `IC_REPO_URL` below.\n",
|
| 62 |
+
"\n",
|
| 63 |
+
"**Option B:** zip the local repo (`Compress-Archive -Path .\\rl-agent, .\\colab -DestinationPath ic.zip`) and drag it into Colab's `/content/` folder as `incident-commander.zip`.\n",
|
| 64 |
+
"\n",
|
| 65 |
+
"**Option C:** clone from a Hugging Face Space repo via `IC_HF_SPACE`."
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"cell_type": "code",
|
| 70 |
+
"execution_count": null,
|
| 71 |
+
"id": "c065ac22",
|
| 72 |
+
"metadata": {},
|
| 73 |
+
"outputs": [],
|
| 74 |
+
"source": [
|
| 75 |
+
"import os, subprocess, sys, zipfile\n",
|
| 76 |
+
"REPO_DIR = '/content/incident-commander'\n",
|
| 77 |
+
"\n",
|
| 78 |
+
"IC_REPO_URL = os.environ.get('IC_REPO_URL', '')\n",
|
| 79 |
+
"if IC_REPO_URL and not os.path.isdir(REPO_DIR):\n",
|
| 80 |
+
" subprocess.run(['git','clone','--depth','1', IC_REPO_URL, REPO_DIR], check=True)\n",
|
| 81 |
+
"\n",
|
| 82 |
+
"if not os.path.isdir(REPO_DIR) and os.path.exists('/content/incident-commander.zip'):\n",
|
| 83 |
+
" os.makedirs(REPO_DIR, exist_ok=True)\n",
|
| 84 |
+
" with zipfile.ZipFile('/content/incident-commander.zip') as z:\n",
|
| 85 |
+
" z.extractall(REPO_DIR)\n",
|
| 86 |
+
"\n",
|
| 87 |
+
"IC_HF_SPACE = os.environ.get('IC_HF_SPACE', '')\n",
|
| 88 |
+
"if not os.path.isdir(REPO_DIR) and IC_HF_SPACE:\n",
|
| 89 |
+
" subprocess.run(['git','clone',\n",
|
| 90 |
+
" f'https://huggingface.co/spaces/{IC_HF_SPACE}', REPO_DIR], check=True)\n",
|
| 91 |
+
"\n",
|
| 92 |
+
"assert os.path.isdir(REPO_DIR), 'No repo present — pick Option A/B/C above.'\n",
|
| 93 |
+
"%cd /content/incident-commander\n",
|
| 94 |
+
"sys.path.insert(0, '/content/incident-commander')\n",
|
| 95 |
+
"sys.path.insert(0, '/content/incident-commander/rl-agent')"
|
| 96 |
+
]
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"cell_type": "markdown",
|
| 100 |
+
"id": "7ebb2e2c",
|
| 101 |
+
"metadata": {},
|
| 102 |
+
"source": [
|
| 103 |
+
"## 3 · Hugging Face token\n",
|
| 104 |
+
"\n",
|
| 105 |
+
"`HF_TOKEN` powers (a) actor weight downloads and (b) the Qwen2.5-72B critic over the Inference Providers router. Get one at https://huggingface.co/settings/tokens — **Read** scope is enough for training (add **Write** if you want this notebook to push your trained adapter).\n",
|
| 106 |
+
"\n",
|
| 107 |
+
"> **Security:** rotate the two HF tokens that were leaked in earlier chat (`hf_RLF…`, `hf_IBf…`)."
|
| 108 |
+
]
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"cell_type": "code",
|
| 112 |
+
"execution_count": null,
|
| 113 |
+
"id": "09f781f0",
|
| 114 |
+
"metadata": {},
|
| 115 |
+
"outputs": [],
|
| 116 |
+
"source": [
|
| 117 |
+
"import os, getpass\n",
|
| 118 |
+
"if not os.environ.get('HF_TOKEN'):\n",
|
| 119 |
+
" os.environ['HF_TOKEN'] = getpass.getpass('Paste your HF token (hf_…): ')\n",
|
| 120 |
+
"os.environ['HUGGING_FACE_HUB_TOKEN'] = os.environ['HF_TOKEN']\n",
|
| 121 |
+
"os.environ['INCIDENT_COMMANDER_MOCK'] = 'true'\n",
|
| 122 |
+
"from huggingface_hub import login\n",
|
| 123 |
+
"login(os.environ['HF_TOKEN'], add_to_git_credential=False)\n",
|
| 124 |
+
"print('HF login OK')"
|
| 125 |
+
]
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"cell_type": "markdown",
|
| 129 |
+
"id": "5fa944ce",
|
| 130 |
+
"metadata": {},
|
| 131 |
+
"source": [
|
| 132 |
+
"## 4 · Smoke-test the simulator"
|
| 133 |
+
]
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"cell_type": "code",
|
| 137 |
+
"execution_count": null,
|
| 138 |
+
"id": "9882eba7",
|
| 139 |
+
"metadata": {},
|
| 140 |
+
"outputs": [],
|
| 141 |
+
"source": [
|
| 142 |
+
"import sys, glob, collections\n",
|
| 143 |
+
"sys.path.insert(0, '/content/incident-commander/rl-agent')\n",
|
| 144 |
+
"from simulator import SimState, dispatch, load_scenario\n",
|
| 145 |
+
"files = sorted(glob.glob('rl-agent/scenarios/sim/*/*.json'))\n",
|
| 146 |
+
"by_diff = collections.Counter(p.split('/')[-2] for p in files)\n",
|
| 147 |
+
"ok = 0\n",
|
| 148 |
+
"for p in files:\n",
|
| 149 |
+
" s = SimState(); scn = load_scenario(p, s)\n",
|
| 150 |
+
" last = None\n",
|
| 151 |
+
" for step in scn['correct_action_chain']:\n",
|
| 152 |
+
" last = dispatch(step, s)\n",
|
| 153 |
+
" if last and last.ok: ok += 1\n",
|
| 154 |
+
"print(f'{ok}/{len(files)} scenario chains pass. By difficulty: {dict(by_diff)}')"
|
| 155 |
+
]
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"cell_type": "markdown",
|
| 159 |
+
"id": "8b71958d",
|
| 160 |
+
"metadata": {},
|
| 161 |
+
"source": [
|
| 162 |
+
"## 5 · Sanity-check the HF Inference critic"
|
| 163 |
+
]
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"cell_type": "code",
|
| 167 |
+
"execution_count": null,
|
| 168 |
+
"id": "d8d7aea7",
|
| 169 |
+
"metadata": {},
|
| 170 |
+
"outputs": [],
|
| 171 |
+
"source": [
|
| 172 |
+
"from colab.train_lib import LLMCritic\n",
|
| 173 |
+
"critic = LLMCritic(provider='hf', model='Qwen/Qwen2.5-72B-Instruct')\n",
|
| 174 |
+
"v_good = critic.value(\n",
|
| 175 |
+
" observation='{\"task\":\"users_db is dead, agent has not failed over yet\"}',\n",
|
| 176 |
+
" action={'id': 'platform.failover_replica', 'params': {'target': 'users_db'}})\n",
|
| 177 |
+
"v_bad = critic.value(\n",
|
| 178 |
+
" observation='{\"task\":\"users_db is unhealthy, runbook NOT read\"}',\n",
|
| 179 |
+
" action={'id': 'platform.restart_cluster', 'params': {'target': 'users_db'}})\n",
|
| 180 |
+
"print(f'failover V={v_good:.2f} restart-brute V={v_bad:.2f}')\n",
|
| 181 |
+
"assert v_good >= v_bad, 'Critic should rate failover above brute-force restart.'"
|
| 182 |
+
]
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"cell_type": "markdown",
|
| 186 |
+
"id": "2df84fc0",
|
| 187 |
+
"metadata": {},
|
| 188 |
+
"source": [
|
| 189 |
+
"## 6 · Configure + run training"
|
| 190 |
+
]
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"cell_type": "code",
|
| 194 |
+
"execution_count": null,
|
| 195 |
+
"id": "b10d4f34",
|
| 196 |
+
"metadata": {},
|
| 197 |
+
"outputs": [],
|
| 198 |
+
"source": [
|
| 199 |
+
"from colab.train_lib import CFG, train_loop\n",
|
| 200 |
+
"CFG.update({\n",
|
| 201 |
+
" 'total_updates': 40, # bump to 80–120 for the real run\n",
|
| 202 |
+
" 'rollouts_per_update': 4,\n",
|
| 203 |
+
" 'max_steps_per_ep': 14,\n",
|
| 204 |
+
" 'critic_provider': 'hf',\n",
|
| 205 |
+
" 'critic_model': 'Qwen/Qwen2.5-72B-Instruct',\n",
|
| 206 |
+
" 'lr': 1e-5,\n",
|
| 207 |
+
" 'run_name': 'demo01',\n",
|
| 208 |
+
" 'tasks': [\n",
|
| 209 |
+
" 'sim_easy_lambda_throttle_001',\n",
|
| 210 |
+
" 'sim_med_eb_lambda_016',\n",
|
| 211 |
+
" 'sim_hard_apigw_chain_001',\n",
|
| 212 |
+
" 'sim_advanced_cascade_users_db_001',\n",
|
| 213 |
+
" 'sim_advanced_runbook_trap_postgres_001',\n",
|
| 214 |
+
" 'sim_advanced_trolley_orders_db_001',\n",
|
| 215 |
+
" 'sim_advanced_saboteur_duel_001',\n",
|
| 216 |
+
" 'sim_advanced_slack_redherring_001',\n",
|
| 217 |
+
" 'sim_gen_app_leak_checkout_005',\n",
|
| 218 |
+
" 'sim_gen_db_duel_users_db_003',\n",
|
| 219 |
+
" 'sim_gen_redherring_payments_007',\n",
|
| 220 |
+
" 'sim_gen_cascade_payments_db_004',\n",
|
| 221 |
+
" ],\n",
|
| 222 |
+
"})\n",
|
| 223 |
+
"log_path = train_loop()\n",
|
| 224 |
+
"print('Training log:', log_path)"
|
| 225 |
+
]
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"cell_type": "markdown",
|
| 229 |
+
"id": "436e2f1f",
|
| 230 |
+
"metadata": {},
|
| 231 |
+
"source": [
|
| 232 |
+
"## 7 · Quick visualization of training curves"
|
| 233 |
+
]
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"cell_type": "code",
|
| 237 |
+
"execution_count": null,
|
| 238 |
+
"id": "a0de13c8",
|
| 239 |
+
"metadata": {},
|
| 240 |
+
"outputs": [],
|
| 241 |
+
"source": [
|
| 242 |
+
"import json, matplotlib.pyplot as plt\n",
|
| 243 |
+
"data = json.load(open(log_path))\n",
|
| 244 |
+
"u = [e['update'] for e in data['updates']]\n",
|
| 245 |
+
"r = [e['mean_reward'] for e in data['updates']]\n",
|
| 246 |
+
"v = [e['mean_value'] for e in data['updates']]\n",
|
| 247 |
+
"k = [e['ppo']['kl'] for e in data['updates']]\n",
|
| 248 |
+
"fig, ax = plt.subplots(1, 3, figsize=(14, 3.5))\n",
|
| 249 |
+
"ax[0].plot(u, r, color='#3fb950'); ax[0].set_title('mean_reward'); ax[0].grid(alpha=.3)\n",
|
| 250 |
+
"ax[1].plot(u, v, color='#58a6ff'); ax[1].set_title('mean_value (Qwen-72B critic)'); ax[1].grid(alpha=.3)\n",
|
| 251 |
+
"ax[2].plot(u, k, color='#f85149'); ax[2].set_title('PPO KL'); ax[2].grid(alpha=.3)\n",
|
| 252 |
+
"for a in ax: a.set_xlabel('update')\n",
|
| 253 |
+
"plt.tight_layout(); plt.show()"
|
| 254 |
+
]
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"cell_type": "markdown",
|
| 258 |
+
"id": "aed4e7bb",
|
| 259 |
+
"metadata": {},
|
| 260 |
+
"source": [
|
| 261 |
+
"## 8 · Replay artifact with the trained agent"
|
| 262 |
+
]
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"cell_type": "code",
|
| 266 |
+
"execution_count": null,
|
| 267 |
+
"id": "c09d3e2f",
|
| 268 |
+
"metadata": {},
|
| 269 |
+
"outputs": [],
|
| 270 |
+
"source": [
|
| 271 |
+
"import glob\n",
|
| 272 |
+
"from colab.train_lib import IncidentRolloutCollector, QwenActor, LLMCritic, CFG\n",
|
| 273 |
+
"actor = QwenActor(model_name=CFG['actor_model'], max_seq_len=CFG['max_seq_len'],\n",
|
| 274 |
+
" lora_r=CFG['lora_r'], lora_alpha=CFG['lora_alpha'],\n",
|
| 275 |
+
" lora_dropout=CFG['lora_dropout'])\n",
|
| 276 |
+
"ckpts = sorted(glob.glob('colab/logs/adapter_*_final')) or sorted(glob.glob('colab/logs/adapter_*'))\n",
|
| 277 |
+
"if ckpts:\n",
|
| 278 |
+
" actor.model.load_adapter(ckpts[-1], adapter_name='trained')\n",
|
| 279 |
+
" actor.model.set_adapter('trained')\n",
|
| 280 |
+
" print('Loaded', ckpts[-1])\n",
|
| 281 |
+
"critic = LLMCritic(provider=CFG['critic_provider'], model=CFG['critic_model'])\n",
|
| 282 |
+
"collector = IncidentRolloutCollector(actor, critic,\n",
|
| 283 |
+
" tasks=['sim_advanced_saboteur_duel_001'],\n",
|
| 284 |
+
" max_steps_per_ep=14)\n",
|
| 285 |
+
"_ = collector.collect(1)\n",
|
| 286 |
+
"replays = sorted(glob.glob('rl-agent/replays/*.html'))\n",
|
| 287 |
+
"print('Latest replay:', replays[-1] if replays else 'none')"
|
| 288 |
+
]
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"cell_type": "markdown",
|
| 292 |
+
"id": "264bdb62",
|
| 293 |
+
"metadata": {},
|
| 294 |
+
"source": [
|
| 295 |
+
"## 9 · Push trained adapter + logs back to Hugging Face\n",
|
| 296 |
+
"\n",
|
| 297 |
+
"Set `IC_HF_USER` to your HF username before running. Creates (or re-uses) a public model repo `<user>/incident-commander-actor`."
|
| 298 |
+
]
|
| 299 |
+
},
|
| 300 |
+
{
|
| 301 |
+
"cell_type": "code",
|
| 302 |
+
"execution_count": null,
|
| 303 |
+
"id": "1ada14fa",
|
| 304 |
+
"metadata": {},
|
| 305 |
+
"outputs": [],
|
| 306 |
+
"source": [
|
| 307 |
+
"import os, glob\n",
|
| 308 |
+
"from huggingface_hub import HfApi, create_repo\n",
|
| 309 |
+
"IC_HF_USER = os.environ.get('IC_HF_USER', '')\n",
|
| 310 |
+
"if not IC_HF_USER:\n",
|
| 311 |
+
" print('Skipping push — set os.environ[\"IC_HF_USER\"] to enable.')\n",
|
| 312 |
+
"else:\n",
|
| 313 |
+
" api = HfApi()\n",
|
| 314 |
+
" repo = f'{IC_HF_USER}/incident-commander-actor'\n",
|
| 315 |
+
" create_repo(repo, exist_ok=True, repo_type='model')\n",
|
| 316 |
+
" final = sorted(glob.glob('colab/logs/adapter_*_final'))[-1]\n",
|
| 317 |
+
" api.upload_folder(folder_path=final, repo_id=repo, repo_type='model',\n",
|
| 318 |
+
" path_in_repo='adapter')\n",
|
| 319 |
+
" api.upload_folder(folder_path='colab/logs', repo_id=repo, repo_type='model',\n",
|
| 320 |
+
" path_in_repo='logs', allow_patterns=['*.json'])\n",
|
| 321 |
+
" api.upload_folder(folder_path='rl-agent/replays', repo_id=repo,\n",
|
| 322 |
+
" repo_type='model', path_in_repo='replays',\n",
|
| 323 |
+
" allow_patterns=['*.html'])\n",
|
| 324 |
+
" print(f'Pushed → https://huggingface.co/{repo}')"
|
| 325 |
+
]
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"cell_type": "markdown",
|
| 329 |
+
"id": "d83a70e2",
|
| 330 |
+
"metadata": {},
|
| 331 |
+
"source": [
|
| 332 |
+
"## 10 · Download artifacts to your laptop"
|
| 333 |
+
]
|
| 334 |
+
},
|
| 335 |
+
{
|
| 336 |
+
"cell_type": "code",
|
| 337 |
+
"execution_count": null,
|
| 338 |
+
"id": "d1e43326",
|
| 339 |
+
"metadata": {},
|
| 340 |
+
"outputs": [],
|
| 341 |
+
"source": [
|
| 342 |
+
"import shutil\n",
|
| 343 |
+
"from google.colab import files\n",
|
| 344 |
+
"shutil.make_archive('/content/ic_artifacts', 'zip',\n",
|
| 345 |
+
" root_dir='/content/incident-commander', base_dir='colab/logs')\n",
|
| 346 |
+
"shutil.make_archive('/content/ic_replays', 'zip',\n",
|
| 347 |
+
" root_dir='/content/incident-commander', base_dir='rl-agent/replays')\n",
|
| 348 |
+
"files.download('/content/ic_artifacts.zip')\n",
|
| 349 |
+
"files.download('/content/ic_replays.zip')"
|
| 350 |
+
]
|
| 351 |
+
}
|
| 352 |
+
],
|
| 353 |
+
"metadata": {
|
| 354 |
+
"language_info": {
|
| 355 |
+
"name": "python"
|
| 356 |
+
}
|
| 357 |
+
},
|
| 358 |
+
"nbformat": 4,
|
| 359 |
+
"nbformat_minor": 5
|
| 360 |
+
}
|
colab/train_lib.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IncidentCommander — Colab training driver.
|
| 2 |
+
|
| 3 |
+
This module is imported by `train_incident_commander.ipynb`. It contains:
|
| 4 |
+
|
| 5 |
+
* `IncidentRolloutCollector` — drives the env, gets the actor's action,
|
| 6 |
+
asks the critic for a value estimate, and produces (s, a, r, s', V).
|
| 7 |
+
* `ClaudeHaikuCritic` — value-function head powered by Anthropic
|
| 8 |
+
Claude Haiku 4.5. The critic returns a scalar in [-1, 1] estimating the
|
| 9 |
+
expected episode return. Cached + batched.
|
| 10 |
+
* `QwenActor` — Qwen3-1.7B loaded via Unsloth + 4-bit
|
| 11 |
+
QLoRA. Generates a JSON action proposal from the env observation.
|
| 12 |
+
* `PPOTrainer` — advanced actor-critic update loop with
|
| 13 |
+
GAE, value-baseline subtraction, KL penalty, and entropy bonus.
|
| 14 |
+
* `train_loop()` — the public entry point used by the
|
| 15 |
+
notebook. Streams metrics to `logs/training_<run>.json` for downstream
|
| 16 |
+
Hugging Face Space visualisations.
|
| 17 |
+
|
| 18 |
+
The implementation is deliberately framework-light so it runs on a free Colab
|
| 19 |
+
T4. If a non-T4 GPU is available it will be used automatically.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import re
|
| 27 |
+
import sys
|
| 28 |
+
import time
|
| 29 |
+
from dataclasses import asdict, dataclass, field
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
from typing import Any, Iterable
|
| 32 |
+
|
| 33 |
+
import torch
|
| 34 |
+
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
# Repository wiring — assumes you cloned the repo and the notebook lives in
|
| 37 |
+
# /content/incident-commander/ on Colab.
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
ROOT = Path(__file__).resolve().parents[1] # repo root
|
| 40 |
+
RL_AGENT = ROOT / "rl-agent"
|
| 41 |
+
sys.path.insert(0, str(RL_AGENT))
|
| 42 |
+
|
| 43 |
+
from environment.env import IncidentCommanderEnv # noqa: E402
|
| 44 |
+
from environment.models import Action, ActionType # noqa: E402
|
| 45 |
+
|
| 46 |
+
LOGS_DIR = ROOT / "colab" / "logs"
|
| 47 |
+
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
# Configuration. Override via `CFG.update({...})` from the notebook.
|
| 51 |
+
# ---------------------------------------------------------------------------
|
| 52 |
+
CFG: dict[str, Any] = {
|
| 53 |
+
"actor_model": "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit",
|
| 54 |
+
# ↑ Closest publicly available 1.5–1.7B Qwen model with a stable
|
| 55 |
+
# Unsloth 4-bit checkpoint. Swap to "unsloth/Qwen3-1.7B-bnb-4bit" if/when
|
| 56 |
+
# the official 4-bit Qwen3-1.7B repo is published.
|
| 57 |
+
"max_seq_len": 3072,
|
| 58 |
+
"lora_r": 16,
|
| 59 |
+
"lora_alpha": 32,
|
| 60 |
+
"lora_dropout": 0.0,
|
| 61 |
+
|
| 62 |
+
# Critic — uses the Hugging Face Inference Providers router (free with a
|
| 63 |
+
# standard HF_TOKEN). Default is Qwen2.5-72B-Instruct: 48× larger than the
|
| 64 |
+
# actor, strong instruction-following, and routinely provisioned on the
|
| 65 |
+
# router for free credits. Alternates: meta-llama/Meta-Llama-3.1-70B-Instruct,
|
| 66 |
+
# mistralai/Mistral-Large-2407.
|
| 67 |
+
"critic_provider": "hf",
|
| 68 |
+
"critic_model": "Qwen/Qwen2.5-72B-Instruct",
|
| 69 |
+
"critic_cache": True,
|
| 70 |
+
"critic_max_tokens": 24,
|
| 71 |
+
"critic_temperature": 0.0,
|
| 72 |
+
|
| 73 |
+
"rollouts_per_update": 4,
|
| 74 |
+
"max_steps_per_ep": 16,
|
| 75 |
+
"ppo_epochs": 2,
|
| 76 |
+
"minibatch_size": 4,
|
| 77 |
+
"gamma": 0.95,
|
| 78 |
+
"gae_lambda": 0.92,
|
| 79 |
+
"clip_eps": 0.20,
|
| 80 |
+
"kl_coef": 0.02,
|
| 81 |
+
"entropy_coef": 0.01,
|
| 82 |
+
"lr": 1e-5,
|
| 83 |
+
"max_grad_norm": 1.0,
|
| 84 |
+
|
| 85 |
+
"tasks": [
|
| 86 |
+
"sim_easy_lambda_throttle_001",
|
| 87 |
+
"sim_med_eb_lambda_016",
|
| 88 |
+
"sim_hard_apigw_chain_001",
|
| 89 |
+
"sim_advanced_cascade_users_db_001",
|
| 90 |
+
"sim_advanced_runbook_trap_postgres_001",
|
| 91 |
+
"sim_advanced_trolley_orders_db_001",
|
| 92 |
+
"sim_advanced_saboteur_duel_001",
|
| 93 |
+
"sim_advanced_slack_redherring_001",
|
| 94 |
+
],
|
| 95 |
+
"total_updates": 80,
|
| 96 |
+
"checkpoint_every": 20,
|
| 97 |
+
"run_name": None, # auto-generated if None
|
| 98 |
+
"seed": 42,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
# Critic — large LLM as a learned value head, served free via HF Inference.
|
| 104 |
+
# ---------------------------------------------------------------------------
|
| 105 |
+
|
| 106 |
+
@dataclass
|
| 107 |
+
class LLMCritic:
|
| 108 |
+
"""Score (state, action) → V-estimate ∈ [-1, 1] with a frozen large LLM.
|
| 109 |
+
|
| 110 |
+
Default routing uses the **Hugging Face Inference Providers** API which is
|
| 111 |
+
free for any HF account (rate-limited but generous). It transparently
|
| 112 |
+
routes Qwen / Llama / Mistral 70B-class models through Together, Nebius,
|
| 113 |
+
HF-Inference, etc.
|
| 114 |
+
|
| 115 |
+
Two routing modes are kept for flexibility:
|
| 116 |
+
* `provider="hf"` — the recommended path. Uses HF_TOKEN.
|
| 117 |
+
* `provider="anthropic"` — kept as a stub for users with their own
|
| 118 |
+
Anthropic key. Not the default.
|
| 119 |
+
|
| 120 |
+
The critic is intentionally asymmetric — it is much larger than the actor
|
| 121 |
+
(Qwen2.5-72B vs the 1.5B QLoRA actor), giving the value estimate genuine
|
| 122 |
+
compute headroom while keeping the actor fast to fine-tune.
|
| 123 |
+
"""
|
| 124 |
+
provider: str = "hf"
|
| 125 |
+
model: str = "Qwen/Qwen2.5-72B-Instruct"
|
| 126 |
+
max_tokens: int = 24
|
| 127 |
+
temperature: float = 0.0
|
| 128 |
+
cache: bool = True
|
| 129 |
+
_cache: dict = field(default_factory=dict)
|
| 130 |
+
|
| 131 |
+
_SYSTEM = (
|
| 132 |
+
"You are a senior SRE evaluating an incident-response action. "
|
| 133 |
+
"Given a JSON observation and the action just taken, respond with a "
|
| 134 |
+
"single number in [-1, 1] estimating the expected total episode "
|
| 135 |
+
"reward. Use -1 for catastrophic moves (data loss, brute-force kills, "
|
| 136 |
+
"ignoring runbooks), 0 for neutral inspection, +1 for textbook fixes. "
|
| 137 |
+
"Output ONLY the number, no commentary."
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# -----------------------------------------------------
|
| 141 |
+
def __post_init__(self):
|
| 142 |
+
self.provider = self.provider.lower()
|
| 143 |
+
if self.provider == "hf":
|
| 144 |
+
try:
|
| 145 |
+
from huggingface_hub import InferenceClient
|
| 146 |
+
except ImportError: # pragma: no cover
|
| 147 |
+
raise RuntimeError("pip install huggingface_hub")
|
| 148 |
+
token = os.environ.get("HF_TOKEN", "")
|
| 149 |
+
if not token:
|
| 150 |
+
print("[critic] WARNING: HF_TOKEN not set — calls will fail.",
|
| 151 |
+
file=sys.stderr)
|
| 152 |
+
self._client = InferenceClient(token=token, timeout=60)
|
| 153 |
+
elif self.provider == "anthropic":
|
| 154 |
+
try:
|
| 155 |
+
import anthropic
|
| 156 |
+
except ImportError: # pragma: no cover
|
| 157 |
+
raise RuntimeError(
|
| 158 |
+
"pip install anthropic (also set ANTHROPIC_API_KEY)")
|
| 159 |
+
key = os.environ.get("ANTHROPIC_API_KEY", "")
|
| 160 |
+
self._client = anthropic.Anthropic(api_key=key) if key else None
|
| 161 |
+
else:
|
| 162 |
+
raise ValueError(f"Unknown critic provider: {self.provider}")
|
| 163 |
+
|
| 164 |
+
# -----------------------------------------------------
|
| 165 |
+
def value(self, observation: str, action: dict) -> float:
|
| 166 |
+
prompt = f"OBSERVATION:\n{observation[:1500]}\n\nACTION:\n{json.dumps(action)}"
|
| 167 |
+
if self.cache and prompt in self._cache:
|
| 168 |
+
return self._cache[prompt]
|
| 169 |
+
|
| 170 |
+
text = "0"
|
| 171 |
+
try:
|
| 172 |
+
if self.provider == "hf":
|
| 173 |
+
out = self._client.chat_completion(
|
| 174 |
+
model=self.model,
|
| 175 |
+
messages=[{"role": "system", "content": self._SYSTEM},
|
| 176 |
+
{"role": "user", "content": prompt}],
|
| 177 |
+
max_tokens=self.max_tokens,
|
| 178 |
+
temperature=self.temperature,
|
| 179 |
+
)
|
| 180 |
+
text = out.choices[0].message.content or "0"
|
| 181 |
+
elif self._client is not None:
|
| 182 |
+
resp = self._client.messages.create(
|
| 183 |
+
model=self.model,
|
| 184 |
+
max_tokens=self.max_tokens,
|
| 185 |
+
system=self._SYSTEM,
|
| 186 |
+
messages=[{"role": "user", "content": prompt}],
|
| 187 |
+
)
|
| 188 |
+
text = resp.content[0].text
|
| 189 |
+
except Exception as exc: # noqa: BLE001
|
| 190 |
+
print(f"[critic] call failed ({self.model}): {exc}",
|
| 191 |
+
file=sys.stderr)
|
| 192 |
+
|
| 193 |
+
v = self._parse_score(text)
|
| 194 |
+
if self.cache:
|
| 195 |
+
self._cache[prompt] = v
|
| 196 |
+
return v
|
| 197 |
+
|
| 198 |
+
@staticmethod
|
| 199 |
+
def _parse_score(text: str) -> float:
|
| 200 |
+
m = re.search(r"-?\d+(?:\.\d+)?", text or "")
|
| 201 |
+
if m is None:
|
| 202 |
+
return 0.0
|
| 203 |
+
try:
|
| 204 |
+
return max(-1.0, min(1.0, float(m.group())))
|
| 205 |
+
except ValueError:
|
| 206 |
+
return 0.0
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# Backwards-compat alias so old notebook cells keep working.
|
| 210 |
+
ClaudeHaikuCritic = LLMCritic
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# ---------------------------------------------------------------------------
|
| 214 |
+
# Actor — Qwen3-1.7B / Qwen2.5-1.5B via Unsloth + QLoRA.
|
| 215 |
+
# ---------------------------------------------------------------------------
|
| 216 |
+
|
| 217 |
+
class QwenActor:
|
| 218 |
+
"""Unsloth-accelerated 4-bit Qwen with LoRA adapters trainable in PPO."""
|
| 219 |
+
|
| 220 |
+
SYSTEM_PROMPT = (
|
| 221 |
+
"You are IncidentCommander, an autonomous SRE. Given the current "
|
| 222 |
+
"observation, decide the next action. Respond with a single JSON "
|
| 223 |
+
"object on one line:\n"
|
| 224 |
+
' {"id": "<service>.<verb>", "params": {...}}\n'
|
| 225 |
+
"where <service>.<verb> is one of the platform.* verbs "
|
| 226 |
+
"(search_runbook, read_runbook, get_logs, get_metrics, get_trace, "
|
| 227 |
+
"read_slack, pause_health_checks, resume_health_checks, "
|
| 228 |
+
"failover_replica, vacuum_freeze_db, warm_cache, "
|
| 229 |
+
"rollback_deployment, rebuild_index, restore_from_backup, "
|
| 230 |
+
"capture_memory_dump) or any AWS service.verb action. NO prose."
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
def __init__(self, *, model_name: str, max_seq_len: int,
|
| 234 |
+
lora_r: int, lora_alpha: int, lora_dropout: float):
|
| 235 |
+
from unsloth import FastLanguageModel
|
| 236 |
+
self.model, self.tokenizer = FastLanguageModel.from_pretrained(
|
| 237 |
+
model_name=model_name,
|
| 238 |
+
max_seq_length=max_seq_len,
|
| 239 |
+
dtype=None, # auto (bf16 on A100, fp16 on T4)
|
| 240 |
+
load_in_4bit=True,
|
| 241 |
+
)
|
| 242 |
+
self.model = FastLanguageModel.get_peft_model(
|
| 243 |
+
self.model,
|
| 244 |
+
r=lora_r,
|
| 245 |
+
lora_alpha=lora_alpha,
|
| 246 |
+
lora_dropout=lora_dropout,
|
| 247 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
| 248 |
+
"gate_proj", "up_proj", "down_proj"],
|
| 249 |
+
bias="none",
|
| 250 |
+
use_gradient_checkpointing="unsloth",
|
| 251 |
+
random_state=3407,
|
| 252 |
+
)
|
| 253 |
+
FastLanguageModel.for_inference(self.model)
|
| 254 |
+
self._train_mode = False
|
| 255 |
+
self.max_seq_len = max_seq_len
|
| 256 |
+
|
| 257 |
+
# -----------------------------------------------------
|
| 258 |
+
def _format(self, observation: str) -> str:
|
| 259 |
+
msgs = [{"role": "system", "content": self.SYSTEM_PROMPT},
|
| 260 |
+
{"role": "user", "content": observation}]
|
| 261 |
+
return self.tokenizer.apply_chat_template(
|
| 262 |
+
msgs, tokenize=False, add_generation_prompt=True)
|
| 263 |
+
|
| 264 |
+
# -----------------------------------------------------
|
| 265 |
+
@torch.inference_mode()
|
| 266 |
+
def act(self, observation: str, *, temperature: float = 0.7) \
|
| 267 |
+
-> tuple[dict, dict]:
|
| 268 |
+
"""Sample an action from the actor. Returns (action, meta) where
|
| 269 |
+
meta contains the prompt/response strings + log-prob sum used by PPO."""
|
| 270 |
+
prompt = self._format(observation)
|
| 271 |
+
ids = self.tokenizer(prompt, return_tensors="pt",
|
| 272 |
+
truncation=True, max_length=self.max_seq_len - 96
|
| 273 |
+
).to(self.model.device)
|
| 274 |
+
out = self.model.generate(
|
| 275 |
+
**ids,
|
| 276 |
+
max_new_tokens=96,
|
| 277 |
+
temperature=temperature,
|
| 278 |
+
do_sample=temperature > 0.0,
|
| 279 |
+
top_p=0.95,
|
| 280 |
+
pad_token_id=self.tokenizer.eos_token_id,
|
| 281 |
+
return_dict_in_generate=True,
|
| 282 |
+
output_scores=True,
|
| 283 |
+
)
|
| 284 |
+
gen_ids = out.sequences[0, ids.input_ids.shape[1]:]
|
| 285 |
+
text = self.tokenizer.decode(gen_ids, skip_special_tokens=True)
|
| 286 |
+
action = self._extract_json(text)
|
| 287 |
+
|
| 288 |
+
# Sum of token-level log-probs of the sampled response — used as the
|
| 289 |
+
# behaviour-policy "old log prob" by PPO.
|
| 290 |
+
scores = torch.stack(out.scores, dim=0).log_softmax(-1)
|
| 291 |
+
chosen = gen_ids.unsqueeze(-1)
|
| 292 |
+
# Trim score steps to the actual generated length.
|
| 293 |
+
scores = scores[: chosen.shape[0]]
|
| 294 |
+
logp_tok = scores.gather(-1, chosen.unsqueeze(0).transpose(0, 1)
|
| 295 |
+
.squeeze(-1).unsqueeze(-1)).squeeze(-1)
|
| 296 |
+
old_logp = float(logp_tok.sum().item())
|
| 297 |
+
return action, {"prompt": prompt, "response": text,
|
| 298 |
+
"old_logp": old_logp,
|
| 299 |
+
"n_tokens": int(gen_ids.shape[0])}
|
| 300 |
+
|
| 301 |
+
# -----------------------------------------------------
|
| 302 |
+
@staticmethod
|
| 303 |
+
def _extract_json(text: str) -> dict:
|
| 304 |
+
m = re.search(r"\{.*\}", text, flags=re.S)
|
| 305 |
+
if m is None:
|
| 306 |
+
return {"id": "platform.get_logs", "params": {"service": "frontend"}}
|
| 307 |
+
try:
|
| 308 |
+
obj = json.loads(m.group())
|
| 309 |
+
if "id" not in obj:
|
| 310 |
+
return {"id": "platform.get_logs", "params": {"service": "frontend"}}
|
| 311 |
+
obj.setdefault("params", {})
|
| 312 |
+
return obj
|
| 313 |
+
except json.JSONDecodeError:
|
| 314 |
+
return {"id": "platform.get_logs", "params": {"service": "frontend"}}
|
| 315 |
+
|
| 316 |
+
# -----------------------------------------------------
|
| 317 |
+
def logp_of(self, prompt: str, response: str) -> torch.Tensor:
|
| 318 |
+
"""Compute log-prob of `response` under the *current* policy. Used
|
| 319 |
+
for the PPO ratio. Differentiable."""
|
| 320 |
+
from unsloth import FastLanguageModel
|
| 321 |
+
FastLanguageModel.for_training(self.model)
|
| 322 |
+
text = prompt + response
|
| 323 |
+
ids = self.tokenizer(text, return_tensors="pt",
|
| 324 |
+
truncation=True, max_length=self.max_seq_len
|
| 325 |
+
).to(self.model.device)
|
| 326 |
+
plen = self.tokenizer(prompt, return_tensors="pt",
|
| 327 |
+
truncation=True, max_length=self.max_seq_len
|
| 328 |
+
).input_ids.shape[1]
|
| 329 |
+
labels = ids.input_ids.clone()
|
| 330 |
+
labels[:, :plen] = -100 # don't score the prompt
|
| 331 |
+
out = self.model(**ids, labels=labels)
|
| 332 |
+
# `loss` is mean over response tokens; convert to summed log-prob.
|
| 333 |
+
n = (labels != -100).sum().item()
|
| 334 |
+
return -out.loss * n
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# ---------------------------------------------------------------------------
|
| 338 |
+
# Rollout collection.
|
| 339 |
+
# ---------------------------------------------------------------------------
|
| 340 |
+
|
| 341 |
+
@dataclass
|
| 342 |
+
class Transition:
|
| 343 |
+
task_id: str
|
| 344 |
+
step: int
|
| 345 |
+
obs: str
|
| 346 |
+
prompt: str
|
| 347 |
+
response: str
|
| 348 |
+
action: dict
|
| 349 |
+
reward: float
|
| 350 |
+
value: float
|
| 351 |
+
old_logp: float
|
| 352 |
+
done: bool
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@dataclass
|
| 356 |
+
class IncidentRolloutCollector:
|
| 357 |
+
actor: QwenActor
|
| 358 |
+
critic: LLMCritic
|
| 359 |
+
tasks: list[str]
|
| 360 |
+
max_steps_per_ep: int = 16
|
| 361 |
+
|
| 362 |
+
def collect(self, n_episodes: int) -> list[Transition]:
|
| 363 |
+
env = IncidentCommanderEnv(use_mock=True)
|
| 364 |
+
transitions: list[Transition] = []
|
| 365 |
+
for ep in range(n_episodes):
|
| 366 |
+
tid = self.tasks[ep % len(self.tasks)]
|
| 367 |
+
obs_struct = env.reset(tid)
|
| 368 |
+
obs_text = self._obs_to_text(env, obs_struct)
|
| 369 |
+
for t in range(self.max_steps_per_ep):
|
| 370 |
+
action, meta = self.actor.act(obs_text)
|
| 371 |
+
value = self.critic.value(obs_text, action)
|
| 372 |
+
step_result = self._step_action(env, action)
|
| 373 |
+
done = step_result.done or t == self.max_steps_per_ep - 1
|
| 374 |
+
transitions.append(Transition(
|
| 375 |
+
task_id=tid, step=t, obs=obs_text,
|
| 376 |
+
prompt=meta["prompt"], response=meta["response"],
|
| 377 |
+
action=action, reward=float(step_result.reward),
|
| 378 |
+
value=value, old_logp=meta["old_logp"], done=done))
|
| 379 |
+
obs_text = self._obs_to_text(env, step_result.observation)
|
| 380 |
+
if done:
|
| 381 |
+
break
|
| 382 |
+
return transitions
|
| 383 |
+
|
| 384 |
+
# -----------------------------------------------------
|
| 385 |
+
@staticmethod
|
| 386 |
+
def _obs_to_text(env, obs) -> str:
|
| 387 |
+
"""Compact textual observation for the LLM."""
|
| 388 |
+
info = {
|
| 389 |
+
"task_id": env._task.task_id if env._task else None,
|
| 390 |
+
"step": env._step_count,
|
| 391 |
+
"blast": getattr(obs, "blast_radius_pct", 0.0),
|
| 392 |
+
"alerts": [a.title for a in getattr(obs, "alerts", [])][:3],
|
| 393 |
+
}
|
| 394 |
+
if env._sim_active and env._sim_state is not None:
|
| 395 |
+
topo = env._sim_state.topology
|
| 396 |
+
info["unhealthy"] = [n for n, x in topo.nodes.items()
|
| 397 |
+
if x.status != "healthy"]
|
| 398 |
+
if env._sim_state.slack is not None:
|
| 399 |
+
info["slack"] = [m.text for m in env._sim_state.slack.recent(3)]
|
| 400 |
+
info["sab_phase"] = (env._sim_state.saboteur._phase
|
| 401 |
+
if env._sim_state.saboteur else None)
|
| 402 |
+
return json.dumps(info)
|
| 403 |
+
|
| 404 |
+
# -----------------------------------------------------
|
| 405 |
+
@staticmethod
|
| 406 |
+
def _step_action(env, action: dict):
|
| 407 |
+
sid = action.get("id", "")
|
| 408 |
+
svc, _, verb = sid.partition(".")
|
| 409 |
+
params = {"service": svc, "verb": verb, **action.get("params", {})}
|
| 410 |
+
return env.step(Action(type=ActionType.AWS_API_CALL, params=params))
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
# ---------------------------------------------------------------------------
|
| 414 |
+
# Advantage computation.
|
| 415 |
+
# ---------------------------------------------------------------------------
|
| 416 |
+
def compute_gae(transitions: list[Transition],
|
| 417 |
+
gamma: float, lam: float) -> list[tuple[float, float]]:
|
| 418 |
+
"""Returns list of (advantage, return) per transition."""
|
| 419 |
+
advs: list[float] = [0.0] * len(transitions)
|
| 420 |
+
returns: list[float] = [0.0] * len(transitions)
|
| 421 |
+
gae = 0.0
|
| 422 |
+
next_v = 0.0
|
| 423 |
+
for i in reversed(range(len(transitions))):
|
| 424 |
+
tr = transitions[i]
|
| 425 |
+
if tr.done:
|
| 426 |
+
next_v, gae = 0.0, 0.0
|
| 427 |
+
delta = tr.reward + gamma * next_v - tr.value
|
| 428 |
+
gae = delta + gamma * lam * gae
|
| 429 |
+
advs[i] = gae
|
| 430 |
+
returns[i] = gae + tr.value
|
| 431 |
+
next_v = tr.value
|
| 432 |
+
return list(zip(advs, returns))
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
# ---------------------------------------------------------------------------
|
| 436 |
+
# PPO update.
|
| 437 |
+
# ---------------------------------------------------------------------------
|
| 438 |
+
class PPOTrainer:
|
| 439 |
+
def __init__(self, actor: QwenActor, *, lr: float, clip_eps: float,
|
| 440 |
+
entropy_coef: float, kl_coef: float, max_grad_norm: float):
|
| 441 |
+
self.actor = actor
|
| 442 |
+
self.opt = torch.optim.AdamW(
|
| 443 |
+
[p for p in actor.model.parameters() if p.requires_grad], lr=lr)
|
| 444 |
+
self.clip_eps = clip_eps
|
| 445 |
+
self.kl_coef = kl_coef
|
| 446 |
+
self.entropy_coef= entropy_coef
|
| 447 |
+
self.max_grad_norm = max_grad_norm
|
| 448 |
+
|
| 449 |
+
def update(self, transitions: list[Transition],
|
| 450 |
+
adv_ret: list[tuple[float, float]],
|
| 451 |
+
*, ppo_epochs: int, minibatch_size: int) -> dict:
|
| 452 |
+
from unsloth import FastLanguageModel
|
| 453 |
+
FastLanguageModel.for_training(self.actor.model)
|
| 454 |
+
device = self.actor.model.device
|
| 455 |
+
|
| 456 |
+
# Normalise advantages.
|
| 457 |
+
advs = torch.tensor([a for a, _ in adv_ret], device=device)
|
| 458 |
+
rets = torch.tensor([r for _, r in adv_ret], device=device)
|
| 459 |
+
advs = (advs - advs.mean()) / (advs.std() + 1e-6)
|
| 460 |
+
old_logps = torch.tensor([t.old_logp for t in transitions], device=device)
|
| 461 |
+
|
| 462 |
+
idxs = list(range(len(transitions)))
|
| 463 |
+
stats: dict[str, list[float]] = {"loss": [], "kl": [],
|
| 464 |
+
"policy_loss": [], "value_err": []}
|
| 465 |
+
for _ in range(ppo_epochs):
|
| 466 |
+
torch.manual_seed(int(time.time()) & 0xFFFF)
|
| 467 |
+
torch.utils._pytree.tree_map(lambda x: x, idxs) # noop for type-checker
|
| 468 |
+
torch.randperm(len(idxs)) # not strictly needed; left as marker
|
| 469 |
+
for start in range(0, len(idxs), minibatch_size):
|
| 470 |
+
batch = idxs[start: start + minibatch_size]
|
| 471 |
+
logps = []
|
| 472 |
+
for i in batch:
|
| 473 |
+
tr = transitions[i]
|
| 474 |
+
logps.append(self.actor.logp_of(tr.prompt, tr.response))
|
| 475 |
+
logp = torch.stack(logps)
|
| 476 |
+
ratio = torch.exp(logp - old_logps[batch])
|
| 477 |
+
a = advs[batch]
|
| 478 |
+
surr1 = ratio * a
|
| 479 |
+
surr2 = torch.clamp(ratio, 1 - self.clip_eps,
|
| 480 |
+
1 + self.clip_eps) * a
|
| 481 |
+
policy_loss = -torch.min(surr1, surr2).mean()
|
| 482 |
+
kl = (old_logps[batch] - logp).mean()
|
| 483 |
+
value_err = ((rets[batch] - torch.tensor(
|
| 484 |
+
[transitions[i].value for i in batch], device=device)) ** 2
|
| 485 |
+
).mean().detach()
|
| 486 |
+
loss = policy_loss + self.kl_coef * kl
|
| 487 |
+
|
| 488 |
+
self.opt.zero_grad()
|
| 489 |
+
loss.backward()
|
| 490 |
+
torch.nn.utils.clip_grad_norm_(
|
| 491 |
+
[p for p in self.actor.model.parameters()
|
| 492 |
+
if p.requires_grad],
|
| 493 |
+
self.max_grad_norm)
|
| 494 |
+
self.opt.step()
|
| 495 |
+
|
| 496 |
+
stats["loss"].append(float(loss.item()))
|
| 497 |
+
stats["policy_loss"].append(float(policy_loss.item()))
|
| 498 |
+
stats["kl"].append(float(kl.item()))
|
| 499 |
+
stats["value_err"].append(float(value_err.item()))
|
| 500 |
+
return {k: float(sum(v) / max(len(v), 1)) for k, v in stats.items()}
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
# ---------------------------------------------------------------------------
|
| 504 |
+
# Public entry point.
|
| 505 |
+
# ---------------------------------------------------------------------------
|
| 506 |
+
def train_loop(cfg: dict | None = None) -> Path:
|
| 507 |
+
"""Run the full training loop. Returns the path to the JSON log."""
|
| 508 |
+
cfg = {**CFG, **(cfg or {})}
|
| 509 |
+
run_name = cfg["run_name"] or f"run_{int(time.time())}"
|
| 510 |
+
log_path = LOGS_DIR / f"training_{run_name}.json"
|
| 511 |
+
print(f"[train] run={run_name} log={log_path}")
|
| 512 |
+
print(f"[train] device CUDA?: {torch.cuda.is_available()} "
|
| 513 |
+
f"name: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else '-'}")
|
| 514 |
+
|
| 515 |
+
actor = QwenActor(model_name=cfg["actor_model"],
|
| 516 |
+
max_seq_len=cfg["max_seq_len"],
|
| 517 |
+
lora_r=cfg["lora_r"], lora_alpha=cfg["lora_alpha"],
|
| 518 |
+
lora_dropout=cfg["lora_dropout"])
|
| 519 |
+
critic = LLMCritic(provider=cfg["critic_provider"],
|
| 520 |
+
model=cfg["critic_model"],
|
| 521 |
+
max_tokens=cfg["critic_max_tokens"],
|
| 522 |
+
temperature=cfg.get("critic_temperature", 0.0),
|
| 523 |
+
cache=cfg["critic_cache"])
|
| 524 |
+
collector = IncidentRolloutCollector(actor, critic, cfg["tasks"],
|
| 525 |
+
max_steps_per_ep=cfg["max_steps_per_ep"])
|
| 526 |
+
trainer = PPOTrainer(actor, lr=cfg["lr"], clip_eps=cfg["clip_eps"],
|
| 527 |
+
entropy_coef=cfg["entropy_coef"],
|
| 528 |
+
kl_coef=cfg["kl_coef"],
|
| 529 |
+
max_grad_norm=cfg["max_grad_norm"])
|
| 530 |
+
|
| 531 |
+
log: dict[str, Any] = {"config": cfg, "updates": []}
|
| 532 |
+
log_path.write_text(json.dumps(log, indent=2))
|
| 533 |
+
|
| 534 |
+
for upd in range(1, cfg["total_updates"] + 1):
|
| 535 |
+
t0 = time.time()
|
| 536 |
+
trans = collector.collect(cfg["rollouts_per_update"])
|
| 537 |
+
adv_ret = compute_gae(trans, cfg["gamma"], cfg["gae_lambda"])
|
| 538 |
+
stats = trainer.update(trans, adv_ret,
|
| 539 |
+
ppo_epochs=cfg["ppo_epochs"],
|
| 540 |
+
minibatch_size=cfg["minibatch_size"])
|
| 541 |
+
ep_rewards: dict[str, list[float]] = {}
|
| 542 |
+
for tr in trans:
|
| 543 |
+
ep_rewards.setdefault(tr.task_id, []).append(tr.reward)
|
| 544 |
+
per_ep = {tid: round(sum(rs), 3) for tid, rs in ep_rewards.items()}
|
| 545 |
+
elapsed = round(time.time() - t0, 2)
|
| 546 |
+
entry = {
|
| 547 |
+
"update": upd,
|
| 548 |
+
"elapsed_s": elapsed,
|
| 549 |
+
"n_transitions": len(trans),
|
| 550 |
+
"mean_reward": round(sum(t.reward for t in trans) / max(len(trans), 1), 4),
|
| 551 |
+
"mean_value": round(sum(t.value for t in trans) / max(len(trans), 1), 4),
|
| 552 |
+
"ppo": stats,
|
| 553 |
+
"rewards_by_task": per_ep,
|
| 554 |
+
}
|
| 555 |
+
log["updates"].append(entry)
|
| 556 |
+
log_path.write_text(json.dumps(log, indent=2))
|
| 557 |
+
print(f"[upd {upd:03d}] reward={entry['mean_reward']:+.3f} "
|
| 558 |
+
f"V̄={entry['mean_value']:+.3f} loss={stats['loss']:+.3f} "
|
| 559 |
+
f"kl={stats['kl']:+.4f} ({elapsed}s)")
|
| 560 |
+
|
| 561 |
+
if upd % cfg["checkpoint_every"] == 0:
|
| 562 |
+
ckpt = LOGS_DIR / f"adapter_{run_name}_u{upd:04d}"
|
| 563 |
+
actor.model.save_pretrained(str(ckpt))
|
| 564 |
+
actor.tokenizer.save_pretrained(str(ckpt))
|
| 565 |
+
print(f"[ckpt] saved {ckpt}")
|
| 566 |
+
|
| 567 |
+
final_ckpt = LOGS_DIR / f"adapter_{run_name}_final"
|
| 568 |
+
actor.model.save_pretrained(str(final_ckpt))
|
| 569 |
+
actor.tokenizer.save_pretrained(str(final_ckpt))
|
| 570 |
+
print(f"[done] final adapter -> {final_ckpt}")
|
| 571 |
+
print(f"[done] JSON log -> {log_path}")
|
| 572 |
+
return log_path
|
rl-agent/environment/aws_actions.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AWS-backed implementations for the new non-obvious ActionType variants.
|
| 2 |
+
|
| 3 |
+
Every helper is a thin, side-effect-aware wrapper around boto3:
|
| 4 |
+
* Read actions return human-readable evidence strings (suitable for
|
| 5 |
+
pasting into the agent's observation `last_action_result`).
|
| 6 |
+
* Write actions return a one-line confirmation; they refuse to run
|
| 7 |
+
unless explicit AWS credentials + the relevant resource pointer
|
| 8 |
+
env var is set, so the unit test suite is safe.
|
| 9 |
+
|
| 10 |
+
Designed so the SAME function is callable from the sync mock path AND
|
| 11 |
+
the async real path — the only async dependency was network IO and we
|
| 12 |
+
guard that with short timeouts via `aws_integrations._client`.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
from datetime import datetime, timedelta, timezone
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
from .aws_integrations import _have_aws_credentials
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _client(service: str):
|
| 26 |
+
"""Region-pinned boto3 client.
|
| 27 |
+
|
| 28 |
+
Always pulls region from `AWS_REGION` (not `AWS_DEFAULT_REGION`) so that
|
| 29 |
+
a stray `~/.aws/config` profile pointing at a different region cannot
|
| 30 |
+
redirect our calls.
|
| 31 |
+
"""
|
| 32 |
+
import boto3 # type: ignore
|
| 33 |
+
from botocore.config import Config # type: ignore
|
| 34 |
+
cfg = Config(connect_timeout=5, read_timeout=15, retries={"max_attempts": 2})
|
| 35 |
+
return boto3.client(service,
|
| 36 |
+
region_name=os.environ.get("AWS_REGION", "us-east-1"),
|
| 37 |
+
config=cfg)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# Resource-shorthand -> (service, accessor) -- the set of "things you can
|
| 41 |
+
# describe a policy of" in our environment. Keeps the agent's parameter
|
| 42 |
+
# surface tiny (just pass shorthand) but the actual call is real.
|
| 43 |
+
_POLICY_TARGETS = {
|
| 44 |
+
"s3:ic-task15": ("s3", "bucket_policy"),
|
| 45 |
+
"s3:checkpoints": ("s3", "bucket_policy"),
|
| 46 |
+
"kms:ic-data-key": ("kms", "key_policy"),
|
| 47 |
+
"secret:ic-stripe-api-key": ("secretsmanager", "resource_policy"),
|
| 48 |
+
"secret:incident-commander/openai-api-key": ("secretsmanager", "resource_policy"),
|
| 49 |
+
"queue:ic-orders": ("sqs", "queue_policy"),
|
| 50 |
+
"queue:ic-orders-dlq": ("sqs", "queue_policy"),
|
| 51 |
+
"queue:ic-events": ("sqs", "queue_policy"),
|
| 52 |
+
"lambda:ic-runbook-restart-pods": ("lambda", "function_policy"),
|
| 53 |
+
"lambda:ic-cold-start-target": ("lambda", "function_policy"),
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _enabled() -> bool:
|
| 58 |
+
return _have_aws_credentials() and bool(os.getenv("AWS_REGION"))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _disabled_msg(action: str) -> str:
|
| 62 |
+
return (f"[skip:{action}] AWS credentials not present; this action "
|
| 63 |
+
f"requires real AWS access. Set AWS_ACCESS_KEY_ID + AWS_REGION.")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
# Read / forensic actions
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
|
| 70 |
+
def check_cloudtrail_events(resource_name: str = "", event_name: str = "",
|
| 71 |
+
last_minutes: int = 60) -> str:
|
| 72 |
+
"""Look up the last N CloudTrail events touching `resource_name` or with
|
| 73 |
+
`event_name`. Useful for tasks where the deformity was caused by an
|
| 74 |
+
operator action (KMS deletion, EventBridge disable, S3 policy change)."""
|
| 75 |
+
if not _enabled():
|
| 76 |
+
return _disabled_msg("check_cloudtrail_events")
|
| 77 |
+
ct = _client("cloudtrail")
|
| 78 |
+
end = datetime.now(timezone.utc)
|
| 79 |
+
start = end - timedelta(minutes=last_minutes)
|
| 80 |
+
attrs = []
|
| 81 |
+
if event_name:
|
| 82 |
+
attrs.append({"AttributeKey": "EventName", "AttributeValue": event_name})
|
| 83 |
+
elif resource_name:
|
| 84 |
+
attrs.append({"AttributeKey": "ResourceName", "AttributeValue": resource_name})
|
| 85 |
+
else:
|
| 86 |
+
return ("error: provide either resource_name or event_name "
|
| 87 |
+
"(e.g. event_name='ScheduleKeyDeletion')")
|
| 88 |
+
try:
|
| 89 |
+
r = ct.lookup_events(LookupAttributes=attrs,
|
| 90 |
+
StartTime=start, EndTime=end, MaxResults=10)
|
| 91 |
+
except Exception as e:
|
| 92 |
+
return f"[cloudtrail] error: {e}"
|
| 93 |
+
rows = []
|
| 94 |
+
for ev in r.get("Events", [])[:10]:
|
| 95 |
+
rows.append(f" {ev.get('EventTime')} {ev.get('EventName')} "
|
| 96 |
+
f"by {ev.get('Username','?')}")
|
| 97 |
+
if not rows:
|
| 98 |
+
return f"[cloudtrail] no events in last {last_minutes}m"
|
| 99 |
+
return "[cloudtrail] " + str(len(rows)) + " events:\n" + "\n".join(rows)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def describe_resource_policy(target: str = "") -> str:
|
| 103 |
+
"""`target` shorthand like 'kms:ic-data-key', 's3:ic-task15',
|
| 104 |
+
'secret:ic-stripe-api-key', 'queue:ic-orders-dlq'."""
|
| 105 |
+
if not _enabled():
|
| 106 |
+
return _disabled_msg("describe_resource_policy")
|
| 107 |
+
spec = _POLICY_TARGETS.get(target)
|
| 108 |
+
if not spec:
|
| 109 |
+
keys = ", ".join(sorted(_POLICY_TARGETS))
|
| 110 |
+
return f"error: unknown target '{target}'. Known: {keys}"
|
| 111 |
+
service, kind = spec
|
| 112 |
+
name = target.split(":", 1)[1]
|
| 113 |
+
try:
|
| 114 |
+
if kind == "bucket_policy":
|
| 115 |
+
bucket = (os.environ.get("S3_DRIFT_BUCKET", name)
|
| 116 |
+
if name == "ic-task15" else
|
| 117 |
+
os.environ.get("S3_CHECKPOINT_BUCKET", name))
|
| 118 |
+
r = _client("s3").get_bucket_policy(Bucket=bucket)
|
| 119 |
+
return f"[bucket-policy {bucket}]\n{r.get('Policy','')[:1500]}"
|
| 120 |
+
if kind == "key_policy":
|
| 121 |
+
kms = _client("kms")
|
| 122 |
+
# GetKeyPolicy refuses aliases; resolve via DescribeKey first.
|
| 123 |
+
key_id = kms.describe_key(KeyId=f"alias/{name}")["KeyMetadata"]["KeyId"]
|
| 124 |
+
r = kms.get_key_policy(KeyId=key_id, PolicyName="default")
|
| 125 |
+
return f"[kms-policy alias/{name} keyId={key_id}]\n{r.get('Policy','')[:1500]}"
|
| 126 |
+
if kind == "resource_policy":
|
| 127 |
+
sm = _client("secretsmanager")
|
| 128 |
+
try:
|
| 129 |
+
r = sm.get_resource_policy(SecretId=name)
|
| 130 |
+
pol = r.get("ResourcePolicy", "(no resource policy attached)")
|
| 131 |
+
except Exception as e:
|
| 132 |
+
pol = f"(no resource policy: {e})"
|
| 133 |
+
tags = sm.describe_secret(SecretId=name).get("Tags", [])
|
| 134 |
+
return f"[secret {name}] tags={tags}\npolicy={pol[:1200]}"
|
| 135 |
+
if kind == "queue_policy":
|
| 136 |
+
sqs = _client("sqs")
|
| 137 |
+
url = sqs.get_queue_url(QueueName=name)["QueueUrl"]
|
| 138 |
+
r = sqs.get_queue_attributes(QueueUrl=url,
|
| 139 |
+
AttributeNames=["Policy",
|
| 140 |
+
"RedrivePolicy"])
|
| 141 |
+
return f"[queue {name}] {json.dumps(r.get('Attributes', {}), indent=2)[:1500]}"
|
| 142 |
+
if kind == "function_policy":
|
| 143 |
+
try:
|
| 144 |
+
r = _client("lambda").get_policy(FunctionName=name)
|
| 145 |
+
return f"[lambda {name}]\n{r.get('Policy','')[:1500]}"
|
| 146 |
+
except Exception as e:
|
| 147 |
+
return f"[lambda {name}] no resource policy ({e})"
|
| 148 |
+
except Exception as e:
|
| 149 |
+
return f"[describe_resource_policy {target}] error: {e}"
|
| 150 |
+
return "error: unhandled policy target"
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def get_quota_usage(service_code: str = "", quota_code: str = "") -> str:
|
| 154 |
+
"""Read Service Quotas for a given service+quota code.
|
| 155 |
+
Common task-relevant codes:
|
| 156 |
+
Bedrock InvokeModel TPM: service_code='bedrock' quota_code='L-XXXXXX'
|
| 157 |
+
DynamoDB account-level RCU: service_code='dynamodb' quota_code='L-F98FE922'
|
| 158 |
+
Lambda concurrent executions: service_code='lambda' quota_code='L-B99A9384'
|
| 159 |
+
Falls back to listing all default quotas for `service_code` when
|
| 160 |
+
`quota_code` is omitted."""
|
| 161 |
+
if not _enabled():
|
| 162 |
+
return _disabled_msg("get_quota_usage")
|
| 163 |
+
if not service_code:
|
| 164 |
+
return "error: service_code is required (e.g. 'lambda', 'dynamodb', 'bedrock')"
|
| 165 |
+
sq = _client("service-quotas")
|
| 166 |
+
try:
|
| 167 |
+
if quota_code:
|
| 168 |
+
r = sq.get_service_quota(ServiceCode=service_code,
|
| 169 |
+
QuotaCode=quota_code)
|
| 170 |
+
q = r.get("Quota", {})
|
| 171 |
+
return (f"[quota] {service_code}/{q.get('QuotaName')} = "
|
| 172 |
+
f"{q.get('Value')} ({q.get('Unit')})")
|
| 173 |
+
r = sq.list_service_quotas(ServiceCode=service_code, MaxResults=10)
|
| 174 |
+
rows = [f" {q['QuotaCode']} {q['QuotaName']} = {q.get('Value')}"
|
| 175 |
+
for q in r.get("Quotas", [])[:10]]
|
| 176 |
+
return f"[quotas {service_code}]\n" + "\n".join(rows)
|
| 177 |
+
except Exception as e:
|
| 178 |
+
return f"[get_quota_usage] error: {e}"
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def check_secret_rotation(secret_name: str = "") -> str:
|
| 182 |
+
if not _enabled():
|
| 183 |
+
return _disabled_msg("check_secret_rotation")
|
| 184 |
+
if not secret_name:
|
| 185 |
+
secret_name = os.environ.get("SECRET_STRIPE_NAME", "ic-stripe-api-key")
|
| 186 |
+
sm = _client("secretsmanager")
|
| 187 |
+
try:
|
| 188 |
+
d = sm.describe_secret(SecretId=secret_name)
|
| 189 |
+
rotation = {
|
| 190 |
+
"RotationEnabled": d.get("RotationEnabled", False),
|
| 191 |
+
"RotationLambdaARN": d.get("RotationLambdaARN"),
|
| 192 |
+
"LastRotatedDate": str(d.get("LastRotatedDate")),
|
| 193 |
+
"LastChangedDate": str(d.get("LastChangedDate")),
|
| 194 |
+
"Tags": {t["Key"]: t["Value"] for t in d.get("Tags", [])},
|
| 195 |
+
}
|
| 196 |
+
return f"[secret-rotation {secret_name}]\n{json.dumps(rotation, indent=2)}"
|
| 197 |
+
except Exception as e:
|
| 198 |
+
return f"[check_secret_rotation {secret_name}] error: {e}"
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def validate_iam_permission(action: str = "", resource: str = "*",
|
| 202 |
+
principal: str = "") -> str:
|
| 203 |
+
"""iam:SimulatePrincipalPolicy — answers 'can principal X do action Y on
|
| 204 |
+
resource Z?'. Defaults principal to the caller's own ARN."""
|
| 205 |
+
if not _enabled():
|
| 206 |
+
return _disabled_msg("validate_iam_permission")
|
| 207 |
+
if not action:
|
| 208 |
+
return "error: action is required (e.g. 's3:PutObject')"
|
| 209 |
+
iam = _client("iam")
|
| 210 |
+
sts = _client("sts")
|
| 211 |
+
try:
|
| 212 |
+
if not principal:
|
| 213 |
+
principal = sts.get_caller_identity()["Arn"]
|
| 214 |
+
sim_kwargs: dict[str, Any] = {
|
| 215 |
+
"PolicySourceArn": principal,
|
| 216 |
+
"ActionNames": [action],
|
| 217 |
+
}
|
| 218 |
+
if resource and resource != "*":
|
| 219 |
+
sim_kwargs["ResourceArns"] = [resource]
|
| 220 |
+
r = iam.simulate_principal_policy(**sim_kwargs)
|
| 221 |
+
rows = []
|
| 222 |
+
for er in r.get("EvaluationResults", []):
|
| 223 |
+
rows.append(f" {er['EvalActionName']} on {er.get('EvalResourceName','*')}"
|
| 224 |
+
f" -> {er['EvalDecision']}")
|
| 225 |
+
return f"[iam-sim principal={principal}]\n" + "\n".join(rows)
|
| 226 |
+
except Exception as e:
|
| 227 |
+
return f"[validate_iam_permission] error: {e}"
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def analyze_cloudwatch_insights(log_group: str = "", pattern: str = "ERROR",
|
| 231 |
+
last_minutes: int = 15) -> str:
|
| 232 |
+
"""Run a small CloudWatch Logs Insights query."""
|
| 233 |
+
if not _enabled():
|
| 234 |
+
return _disabled_msg("analyze_cloudwatch_insights")
|
| 235 |
+
if not log_group:
|
| 236 |
+
log_group = os.environ.get("CLOUDWATCH_LOG_GROUP",
|
| 237 |
+
"/aws/lambda/ic-cold-start-target")
|
| 238 |
+
cw = _client("logs")
|
| 239 |
+
end = int(datetime.now(timezone.utc).timestamp())
|
| 240 |
+
start = end - last_minutes * 60
|
| 241 |
+
query = (f"fields @timestamp, @message"
|
| 242 |
+
f" | filter @message like /{pattern}/"
|
| 243 |
+
f" | sort @timestamp desc | limit 20")
|
| 244 |
+
try:
|
| 245 |
+
qid = cw.start_query(logGroupName=log_group,
|
| 246 |
+
startTime=start, endTime=end,
|
| 247 |
+
queryString=query)["queryId"]
|
| 248 |
+
# Poll up to ~6s.
|
| 249 |
+
import time
|
| 250 |
+
for _ in range(20):
|
| 251 |
+
time.sleep(0.3)
|
| 252 |
+
r = cw.get_query_results(queryId=qid)
|
| 253 |
+
if r["status"] in ("Complete", "Failed", "Cancelled"):
|
| 254 |
+
break
|
| 255 |
+
results = r.get("results", [])
|
| 256 |
+
rows = []
|
| 257 |
+
for row in results[:10]:
|
| 258 |
+
line = " | ".join(f"{f['field']}={f['value']}" for f in row
|
| 259 |
+
if f['field'] != '@ptr')
|
| 260 |
+
rows.append(" " + line)
|
| 261 |
+
return (f"[insights {log_group} pattern={pattern!r}] "
|
| 262 |
+
f"{len(results)} rows\n" + "\n".join(rows))
|
| 263 |
+
except Exception as e:
|
| 264 |
+
return f"[analyze_cloudwatch_insights] error: {e}"
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def inspect_dlq_messages(queue_name: str = "ic-orders-dlq",
|
| 268 |
+
max_messages: int = 5) -> str:
|
| 269 |
+
"""Peek (no-delete) at messages in a DLQ. Uses long-poll=0 + receipt
|
| 270 |
+
abandonment so the visibility timer expires and another consumer can
|
| 271 |
+
re-read. This is a forensic action; PURGE_QUEUE is the corresponding
|
| 272 |
+
write action."""
|
| 273 |
+
if not _enabled():
|
| 274 |
+
return _disabled_msg("inspect_dlq_messages")
|
| 275 |
+
sqs = _client("sqs")
|
| 276 |
+
try:
|
| 277 |
+
url = sqs.get_queue_url(QueueName=queue_name)["QueueUrl"]
|
| 278 |
+
r = sqs.receive_message(QueueUrl=url,
|
| 279 |
+
MaxNumberOfMessages=min(10, max_messages),
|
| 280 |
+
VisibilityTimeout=2,
|
| 281 |
+
WaitTimeSeconds=0,
|
| 282 |
+
AttributeNames=["All"],
|
| 283 |
+
MessageAttributeNames=["All"])
|
| 284 |
+
msgs = r.get("Messages", [])
|
| 285 |
+
attrs = sqs.get_queue_attributes(
|
| 286 |
+
QueueUrl=url,
|
| 287 |
+
AttributeNames=["ApproximateNumberOfMessages"],
|
| 288 |
+
).get("Attributes", {})
|
| 289 |
+
depth = attrs.get("ApproximateNumberOfMessages", "?")
|
| 290 |
+
rows = [f" body[{i}] = {m.get('Body','')[:200]}"
|
| 291 |
+
for i, m in enumerate(msgs)]
|
| 292 |
+
return (f"[dlq {queue_name}] depth={depth} sampled={len(msgs)}\n"
|
| 293 |
+
+ "\n".join(rows))
|
| 294 |
+
except Exception as e:
|
| 295 |
+
return f"[inspect_dlq_messages {queue_name}] error: {e}"
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def diff_config_versions(parameter_name: str = "",
|
| 299 |
+
a: int = 0, b: int = 0) -> str:
|
| 300 |
+
"""SSM parameter version diff. If a/b are 0, picks the latest two."""
|
| 301 |
+
if not _enabled():
|
| 302 |
+
return _disabled_msg("diff_config_versions")
|
| 303 |
+
if not parameter_name:
|
| 304 |
+
parameter_name = os.environ.get("SSM_DB_POOL_PARAM",
|
| 305 |
+
"/ic/payments/db-pool-size")
|
| 306 |
+
ssm = _client("ssm")
|
| 307 |
+
try:
|
| 308 |
+
history = ssm.get_parameter_history(Name=parameter_name,
|
| 309 |
+
WithDecryption=False, MaxResults=10)\
|
| 310 |
+
.get("Parameters", [])
|
| 311 |
+
if not history:
|
| 312 |
+
return f"[ssm {parameter_name}] no history"
|
| 313 |
+
history.sort(key=lambda p: p["Version"], reverse=True)
|
| 314 |
+
if a == 0 or b == 0:
|
| 315 |
+
latest = history[0]
|
| 316 |
+
previous = history[1] if len(history) > 1 else latest
|
| 317 |
+
else:
|
| 318 |
+
latest = next((p for p in history if p["Version"] == a), history[0])
|
| 319 |
+
previous = next((p for p in history if p["Version"] == b), latest)
|
| 320 |
+
return (f"[ssm-diff {parameter_name}]\n"
|
| 321 |
+
f" v{previous['Version']} = {previous['Value']!r} "
|
| 322 |
+
f"({previous.get('LastModifiedDate')})\n"
|
| 323 |
+
f" v{latest['Version']} = {latest['Value']!r} "
|
| 324 |
+
f"({latest.get('LastModifiedDate')})")
|
| 325 |
+
except Exception as e:
|
| 326 |
+
return f"[diff_config_versions {parameter_name}] error: {e}"
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def describe_state_machine_execution(execution_arn: str = "",
|
| 330 |
+
state_machine_name: str = "") -> str:
|
| 331 |
+
"""Inspect the most recent failed execution of a state machine."""
|
| 332 |
+
if not _enabled():
|
| 333 |
+
return _disabled_msg("describe_state_machine_execution")
|
| 334 |
+
sfn = _client("stepfunctions")
|
| 335 |
+
try:
|
| 336 |
+
if not execution_arn:
|
| 337 |
+
sm_name = state_machine_name or "ic-order-saga"
|
| 338 |
+
arn_prefix = (os.environ.get("SFN_ORDER_SAGA_ARN", "")
|
| 339 |
+
or f"arn:aws:states:{os.getenv('AWS_REGION','us-east-1')}"
|
| 340 |
+
f":{_client('sts').get_caller_identity()['Account']}"
|
| 341 |
+
f":stateMachine:{sm_name}")
|
| 342 |
+
r = sfn.list_executions(stateMachineArn=arn_prefix,
|
| 343 |
+
statusFilter="FAILED", maxResults=1)
|
| 344 |
+
execs = r.get("executions", [])
|
| 345 |
+
if not execs:
|
| 346 |
+
return f"[sfn {sm_name}] no FAILED executions in history"
|
| 347 |
+
execution_arn = execs[0]["executionArn"]
|
| 348 |
+
d = sfn.describe_execution(executionArn=execution_arn)
|
| 349 |
+
h = sfn.get_execution_history(executionArn=execution_arn,
|
| 350 |
+
reverseOrder=True, maxResults=5)
|
| 351 |
+
last = h.get("events", [])[:5]
|
| 352 |
+
events = [f" {e['type']} @ {e['timestamp']}" for e in last]
|
| 353 |
+
return (f"[sfn-exec {execution_arn}] status={d.get('status')} "
|
| 354 |
+
f"stop={d.get('stopDate')}\n" + "\n".join(events))
|
| 355 |
+
except Exception as e:
|
| 356 |
+
return f"[describe_state_machine_execution] error: {e}"
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
# ---------------------------------------------------------------------------
|
| 360 |
+
# Write / remediation actions
|
| 361 |
+
# ---------------------------------------------------------------------------
|
| 362 |
+
|
| 363 |
+
def invoke_lambda(function_name: str = "", payload: dict | None = None) -> str:
|
| 364 |
+
if not _enabled():
|
| 365 |
+
return _disabled_msg("invoke_lambda")
|
| 366 |
+
if not function_name:
|
| 367 |
+
function_name = os.environ.get("LAMBDA_RUNBOOK_FUNCTION",
|
| 368 |
+
"ic-runbook-restart-pods")
|
| 369 |
+
lam = _client("lambda")
|
| 370 |
+
try:
|
| 371 |
+
r = lam.invoke(FunctionName=function_name,
|
| 372 |
+
InvocationType="RequestResponse",
|
| 373 |
+
Payload=json.dumps(payload or {}).encode("utf-8"))
|
| 374 |
+
body = r["Payload"].read().decode("utf-8", errors="replace")
|
| 375 |
+
return (f"[invoke_lambda {function_name}] status={r['StatusCode']} "
|
| 376 |
+
f"body={body[:500]}")
|
| 377 |
+
except Exception as e:
|
| 378 |
+
return f"[invoke_lambda {function_name}] error: {e}"
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def rotate_secret(secret_name: str = "") -> str:
|
| 382 |
+
if not _enabled():
|
| 383 |
+
return _disabled_msg("rotate_secret")
|
| 384 |
+
if not secret_name:
|
| 385 |
+
secret_name = os.environ.get("SECRET_STRIPE_NAME", "ic-stripe-api-key")
|
| 386 |
+
sm = _client("secretsmanager")
|
| 387 |
+
try:
|
| 388 |
+
# Most secrets won't have a real rotation lambda \u2014 we still write a
|
| 389 |
+
# fresh value + tag, which is the same evidence the agent needs.
|
| 390 |
+
new_value = json.dumps({"api_key": f"sk_rot_{int(datetime.now().timestamp())}"})
|
| 391 |
+
sm.put_secret_value(SecretId=secret_name, SecretString=new_value)
|
| 392 |
+
sm.tag_resource(SecretId=secret_name,
|
| 393 |
+
Tags=[{"Key": "RotationStatus", "Value": "ROTATED"},
|
| 394 |
+
{"Key": "LastRotatedDays", "Value": "0"}])
|
| 395 |
+
return f"[rotate_secret {secret_name}] new version stored, tags updated"
|
| 396 |
+
except Exception as e:
|
| 397 |
+
return f"[rotate_secret {secret_name}] error: {e}"
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def purge_queue(queue_name: str = "") -> str:
|
| 401 |
+
if not _enabled():
|
| 402 |
+
return _disabled_msg("purge_queue")
|
| 403 |
+
if not queue_name:
|
| 404 |
+
queue_name = os.environ.get("SQS_ORDERS_DLQ_URL", "ic-orders-dlq")\
|
| 405 |
+
.rsplit("/", 1)[-1]
|
| 406 |
+
sqs = _client("sqs")
|
| 407 |
+
try:
|
| 408 |
+
url = sqs.get_queue_url(QueueName=queue_name)["QueueUrl"]
|
| 409 |
+
sqs.purge_queue(QueueUrl=url)
|
| 410 |
+
return f"[purge_queue {queue_name}] purge initiated"
|
| 411 |
+
except Exception as e:
|
| 412 |
+
return f"[purge_queue {queue_name}] error: {e}"
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def enable_eventbridge_rule(rule_name: str = "", bus_name: str = "") -> str:
|
| 416 |
+
if not _enabled():
|
| 417 |
+
return _disabled_msg("enable_eventbridge_rule")
|
| 418 |
+
rule = rule_name or os.environ.get("EVENTBRIDGE_RULE_NAME", "ic-orders-rule")
|
| 419 |
+
bus = bus_name or os.environ.get("EVENTBRIDGE_BUS_NAME", "ic-bus")
|
| 420 |
+
eb = _client("events")
|
| 421 |
+
try:
|
| 422 |
+
eb.enable_rule(Name=rule, EventBusName=bus)
|
| 423 |
+
st = eb.describe_rule(Name=rule, EventBusName=bus).get("State", "?")
|
| 424 |
+
return f"[enable_rule {bus}/{rule}] state={st}"
|
| 425 |
+
except Exception as e:
|
| 426 |
+
return f"[enable_eventbridge_rule {bus}/{rule}] error: {e}"
|
rl-agent/environment/aws_integrations.py
CHANGED
|
@@ -28,13 +28,22 @@ from typing import Any, Optional
|
|
| 28 |
log = logging.getLogger(__name__)
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def _have_aws_credentials() -> bool:
|
| 32 |
"""Fast, non-network check for AWS credentials.
|
| 33 |
|
| 34 |
-
Returns
|
|
|
|
| 35 |
configured. Avoids IMDS calls that block for seconds on a laptop / HF
|
| 36 |
Space with no AWS access.
|
| 37 |
"""
|
|
|
|
|
|
|
| 38 |
if os.getenv("AWS_ACCESS_KEY_ID") and os.getenv("AWS_SECRET_ACCESS_KEY"):
|
| 39 |
return True
|
| 40 |
if os.getenv("AWS_PROFILE"):
|
|
@@ -48,7 +57,14 @@ def _have_aws_credentials() -> bool:
|
|
| 48 |
|
| 49 |
def _client(service: str, region: Optional[str] = None):
|
| 50 |
"""Build a boto3 client with short connect/read timeouts so importing the
|
| 51 |
-
module against a no-creds machine doesn't stall the HF Space.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
import boto3 # type: ignore
|
| 53 |
from botocore.config import Config # type: ignore
|
| 54 |
cfg = Config(connect_timeout=2, read_timeout=3, retries={"max_attempts": 1})
|
|
@@ -345,6 +361,259 @@ class SecretsLoader:
|
|
| 345 |
return False
|
| 346 |
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
# ---------------------------------------------------------------------------
|
| 349 |
# Aggregate status for /aws/status + /dashboard/aws
|
| 350 |
# ---------------------------------------------------------------------------
|
|
@@ -376,6 +645,12 @@ def aws_status() -> dict[str, Any]:
|
|
| 376 |
cwm = CloudWatchMetricsPublisher()
|
| 377 |
sns = SNSIncidentPublisher()
|
| 378 |
sec = SecretsLoader()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
_svc("CloudWatch Logs", cw.enabled, {"log_group": cw.log_group})
|
| 381 |
_svc("S3", s3.enabled, {"bucket": s3.bucket, "prefix": s3.prefix})
|
|
@@ -383,6 +658,12 @@ def aws_status() -> dict[str, Any]:
|
|
| 383 |
_svc("CloudWatch Metrics",cwm.enabled, {"namespace": cwm.namespace})
|
| 384 |
_svc("SNS", sns.enabled, {"topic": sns.topic_arn})
|
| 385 |
_svc("Secrets Manager", sec.enabled, {"configured_secret": os.getenv("OPENAI_SECRET_ARN")})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
_svc("EKS", bool(os.getenv("EKS_CLUSTER_NAME")),
|
| 387 |
{"cluster": os.getenv("EKS_CLUSTER_NAME")})
|
| 388 |
_svc("ECR", bool(os.getenv("ECR_REPOSITORY_URL")),
|
|
|
|
| 28 |
log = logging.getLogger(__name__)
|
| 29 |
|
| 30 |
|
| 31 |
+
# Mentor 2026-04-25: live AWS is OFF by default. Set IC_USE_LIVE_AWS=true to
|
| 32 |
+
# re-enable. When False, every helper in this module reports enabled=False
|
| 33 |
+
# and `_client` raises so that no boto3 call can leak through.
|
| 34 |
+
USE_LIVE_AWS = os.getenv("IC_USE_LIVE_AWS", "false").lower() in ("1", "true", "yes")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
def _have_aws_credentials() -> bool:
|
| 38 |
"""Fast, non-network check for AWS credentials.
|
| 39 |
|
| 40 |
+
Returns False unconditionally when IC_USE_LIVE_AWS is off. Otherwise
|
| 41 |
+
returns True only if env-var credentials are present OR a profile is
|
| 42 |
configured. Avoids IMDS calls that block for seconds on a laptop / HF
|
| 43 |
Space with no AWS access.
|
| 44 |
"""
|
| 45 |
+
if not USE_LIVE_AWS:
|
| 46 |
+
return False
|
| 47 |
if os.getenv("AWS_ACCESS_KEY_ID") and os.getenv("AWS_SECRET_ACCESS_KEY"):
|
| 48 |
return True
|
| 49 |
if os.getenv("AWS_PROFILE"):
|
|
|
|
| 57 |
|
| 58 |
def _client(service: str, region: Optional[str] = None):
|
| 59 |
"""Build a boto3 client with short connect/read timeouts so importing the
|
| 60 |
+
module against a no-creds machine doesn't stall the HF Space.
|
| 61 |
+
|
| 62 |
+
Refuses to construct a client when IC_USE_LIVE_AWS is off (mentor cost
|
| 63 |
+
cutoff 2026-04-25). Set IC_USE_LIVE_AWS=true to re-enable.
|
| 64 |
+
"""
|
| 65 |
+
if not USE_LIVE_AWS:
|
| 66 |
+
raise RuntimeError(
|
| 67 |
+
"live AWS disabled (set IC_USE_LIVE_AWS=true to re-enable)")
|
| 68 |
import boto3 # type: ignore
|
| 69 |
from botocore.config import Config # type: ignore
|
| 70 |
cfg = Config(connect_timeout=2, read_timeout=3, retries={"max_attempts": 1})
|
|
|
|
| 361 |
return False
|
| 362 |
|
| 363 |
|
| 364 |
+
# ---------------------------------------------------------------------------
|
| 365 |
+
# SQS — incident events queue
|
| 366 |
+
# ---------------------------------------------------------------------------
|
| 367 |
+
|
| 368 |
+
class SQSEventPublisher:
|
| 369 |
+
"""Push every agent action onto an SQS queue so a side-car Lambda or the
|
| 370 |
+
next training run can replay them.
|
| 371 |
+
|
| 372 |
+
Enabled when `SQS_QUEUE_URL` is set and boto3 creds resolve.
|
| 373 |
+
"""
|
| 374 |
+
|
| 375 |
+
def __init__(self, queue_url: Optional[str] = None, region: Optional[str] = None):
|
| 376 |
+
self.queue_url = queue_url or os.getenv("SQS_QUEUE_URL")
|
| 377 |
+
self.region = region or os.getenv("AWS_REGION")
|
| 378 |
+
self._client = None
|
| 379 |
+
self.enabled = False
|
| 380 |
+
if self.queue_url and _have_aws_credentials():
|
| 381 |
+
try:
|
| 382 |
+
self._client = _client("sqs", self.region)
|
| 383 |
+
self._client.get_queue_attributes(
|
| 384 |
+
QueueUrl=self.queue_url, AttributeNames=["QueueArn"])
|
| 385 |
+
self.enabled = True
|
| 386 |
+
log.info("SQS publisher: %s", self.queue_url)
|
| 387 |
+
except Exception as e:
|
| 388 |
+
log.warning("SQS disabled (%s)", e)
|
| 389 |
+
|
| 390 |
+
def publish(self, body: dict, group_id: Optional[str] = None) -> bool:
|
| 391 |
+
if not self.enabled or self._client is None:
|
| 392 |
+
return False
|
| 393 |
+
try:
|
| 394 |
+
kwargs: dict[str, Any] = {
|
| 395 |
+
"QueueUrl": self.queue_url,
|
| 396 |
+
"MessageBody": json.dumps(body, default=str),
|
| 397 |
+
}
|
| 398 |
+
if self.queue_url and self.queue_url.endswith(".fifo"):
|
| 399 |
+
kwargs["MessageGroupId"] = group_id or "default"
|
| 400 |
+
kwargs["MessageDeduplicationId"] = f"{group_id or 'd'}-{int(time.time()*1000)}"
|
| 401 |
+
self._client.send_message(**kwargs)
|
| 402 |
+
return True
|
| 403 |
+
except Exception as e:
|
| 404 |
+
log.warning("sqs publish failed: %s", e)
|
| 405 |
+
return False
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# ---------------------------------------------------------------------------
|
| 409 |
+
# Lambda invoker — call a runbook lambda
|
| 410 |
+
# ---------------------------------------------------------------------------
|
| 411 |
+
|
| 412 |
+
class LambdaInvoker:
|
| 413 |
+
"""Synchronously invoke a Lambda function (e.g. a remediation runbook).
|
| 414 |
+
|
| 415 |
+
Enabled when `LAMBDA_RUNBOOK_FUNCTION` is set and boto3 creds resolve.
|
| 416 |
+
"""
|
| 417 |
+
|
| 418 |
+
def __init__(self, function_name: Optional[str] = None, region: Optional[str] = None):
|
| 419 |
+
self.function_name = function_name or os.getenv("LAMBDA_RUNBOOK_FUNCTION")
|
| 420 |
+
self.region = region or os.getenv("AWS_REGION")
|
| 421 |
+
self._client = None
|
| 422 |
+
self.enabled = False
|
| 423 |
+
if self.function_name and _have_aws_credentials():
|
| 424 |
+
try:
|
| 425 |
+
self._client = _client("lambda", self.region)
|
| 426 |
+
self._client.get_function(FunctionName=self.function_name)
|
| 427 |
+
self.enabled = True
|
| 428 |
+
log.info("Lambda invoker: %s", self.function_name)
|
| 429 |
+
except Exception as e:
|
| 430 |
+
log.warning("Lambda disabled (%s)", e)
|
| 431 |
+
|
| 432 |
+
def invoke(self, payload: dict) -> dict:
|
| 433 |
+
if not self.enabled or self._client is None:
|
| 434 |
+
return {"ok": False, "error": "lambda not configured"}
|
| 435 |
+
try:
|
| 436 |
+
resp = self._client.invoke(
|
| 437 |
+
FunctionName=self.function_name,
|
| 438 |
+
InvocationType="RequestResponse",
|
| 439 |
+
Payload=json.dumps(payload, default=str).encode("utf-8"),
|
| 440 |
+
)
|
| 441 |
+
body = resp.get("Payload")
|
| 442 |
+
return {
|
| 443 |
+
"ok": resp.get("StatusCode", 500) < 300,
|
| 444 |
+
"status": resp.get("StatusCode"),
|
| 445 |
+
"body": body.read().decode("utf-8", errors="replace") if body else "",
|
| 446 |
+
"function_error": resp.get("FunctionError"),
|
| 447 |
+
}
|
| 448 |
+
except Exception as e:
|
| 449 |
+
log.warning("lambda invoke failed: %s", e)
|
| 450 |
+
return {"ok": False, "error": str(e)}
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
# ---------------------------------------------------------------------------
|
| 454 |
+
# EventBridge publisher
|
| 455 |
+
# ---------------------------------------------------------------------------
|
| 456 |
+
|
| 457 |
+
class EventBridgePublisher:
|
| 458 |
+
"""Publish a structured event to an EventBridge bus so other workloads
|
| 459 |
+
(e.g. PagerDuty integration) can react.
|
| 460 |
+
|
| 461 |
+
Enabled when `EVENTBRIDGE_BUS_NAME` is set and boto3 creds resolve.
|
| 462 |
+
"""
|
| 463 |
+
|
| 464 |
+
def __init__(self, bus_name: Optional[str] = None, region: Optional[str] = None):
|
| 465 |
+
self.bus_name = bus_name or os.getenv("EVENTBRIDGE_BUS_NAME", "default")
|
| 466 |
+
self.source = os.getenv("EVENTBRIDGE_SOURCE", "incident-commander.agent")
|
| 467 |
+
self.region = region or os.getenv("AWS_REGION")
|
| 468 |
+
self._client = None
|
| 469 |
+
self.enabled = False
|
| 470 |
+
if _have_aws_credentials():
|
| 471 |
+
try:
|
| 472 |
+
self._client = _client("events", self.region)
|
| 473 |
+
self._client.describe_event_bus(Name=self.bus_name)
|
| 474 |
+
self.enabled = True
|
| 475 |
+
log.info("EventBridge publisher: bus=%s source=%s",
|
| 476 |
+
self.bus_name, self.source)
|
| 477 |
+
except Exception as e:
|
| 478 |
+
log.warning("EventBridge disabled (%s)", e)
|
| 479 |
+
|
| 480 |
+
def publish(self, detail_type: str, detail: dict) -> bool:
|
| 481 |
+
if not self.enabled or self._client is None:
|
| 482 |
+
return False
|
| 483 |
+
try:
|
| 484 |
+
self._client.put_events(Entries=[{
|
| 485 |
+
"Source": self.source,
|
| 486 |
+
"DetailType": detail_type,
|
| 487 |
+
"Detail": json.dumps(detail, default=str),
|
| 488 |
+
"EventBusName": self.bus_name,
|
| 489 |
+
}])
|
| 490 |
+
return True
|
| 491 |
+
except Exception as e:
|
| 492 |
+
log.warning("eventbridge put_events failed: %s", e)
|
| 493 |
+
return False
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
# ---------------------------------------------------------------------------
|
| 497 |
+
# X-Ray trace reader (read-only)
|
| 498 |
+
# ---------------------------------------------------------------------------
|
| 499 |
+
|
| 500 |
+
class XRayTraceReader:
|
| 501 |
+
"""List recent X-Ray trace summaries so the agent can correlate latency
|
| 502 |
+
spikes with downstream calls.
|
| 503 |
+
|
| 504 |
+
Enabled when `XRAY_ENABLED` is truthy and boto3 creds resolve.
|
| 505 |
+
"""
|
| 506 |
+
|
| 507 |
+
def __init__(self, region: Optional[str] = None):
|
| 508 |
+
self.region = region or os.getenv("AWS_REGION")
|
| 509 |
+
self._client = None
|
| 510 |
+
self.enabled = False
|
| 511 |
+
if os.getenv("XRAY_ENABLED", "").lower() in ("1", "true", "yes") \
|
| 512 |
+
and _have_aws_credentials():
|
| 513 |
+
try:
|
| 514 |
+
self._client = _client("xray", self.region)
|
| 515 |
+
# cheap probe — list_groups is always allowed if xray:* granted
|
| 516 |
+
self._client.get_groups()
|
| 517 |
+
self.enabled = True
|
| 518 |
+
log.info("X-Ray reader enabled")
|
| 519 |
+
except Exception as e:
|
| 520 |
+
log.warning("X-Ray disabled (%s)", e)
|
| 521 |
+
|
| 522 |
+
def recent_traces(self, last_minutes: int = 5, limit: int = 20) -> list[dict]:
|
| 523 |
+
if not self.enabled or self._client is None:
|
| 524 |
+
return []
|
| 525 |
+
from datetime import datetime as _dt, timezone as _tz, timedelta as _td
|
| 526 |
+
end = _dt.now(_tz.utc)
|
| 527 |
+
start = end - _td(minutes=last_minutes)
|
| 528 |
+
try:
|
| 529 |
+
resp = self._client.get_trace_summaries(StartTime=start, EndTime=end)
|
| 530 |
+
out: list[dict] = []
|
| 531 |
+
for s in resp.get("TraceSummaries", [])[:limit]:
|
| 532 |
+
out.append({
|
| 533 |
+
"id": s.get("Id"),
|
| 534 |
+
"duration": s.get("Duration"),
|
| 535 |
+
"has_error": bool(s.get("HasError")),
|
| 536 |
+
"has_fault": bool(s.get("HasFault")),
|
| 537 |
+
"http": s.get("Http", {}),
|
| 538 |
+
})
|
| 539 |
+
return out
|
| 540 |
+
except Exception as e:
|
| 541 |
+
log.warning("xray get_trace_summaries failed: %s", e)
|
| 542 |
+
return []
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
# ---------------------------------------------------------------------------
|
| 546 |
+
# SSM Parameter Store reader
|
| 547 |
+
# ---------------------------------------------------------------------------
|
| 548 |
+
|
| 549 |
+
class SSMParameterReader:
|
| 550 |
+
"""Read SSM parameters used as runtime feature flags / config values.
|
| 551 |
+
|
| 552 |
+
Enabled when boto3 creds resolve. Fails closed (returns None) on errors.
|
| 553 |
+
"""
|
| 554 |
+
|
| 555 |
+
def __init__(self, region: Optional[str] = None):
|
| 556 |
+
self.region = region or os.getenv("AWS_REGION")
|
| 557 |
+
self._client = None
|
| 558 |
+
self.enabled = False
|
| 559 |
+
if _have_aws_credentials():
|
| 560 |
+
try:
|
| 561 |
+
self._client = _client("ssm", self.region)
|
| 562 |
+
self.enabled = True
|
| 563 |
+
log.info("SSM parameter reader enabled")
|
| 564 |
+
except Exception as e:
|
| 565 |
+
log.warning("SSM disabled (%s)", e)
|
| 566 |
+
|
| 567 |
+
def get(self, name: str, with_decryption: bool = True) -> Optional[str]:
|
| 568 |
+
if not self.enabled or self._client is None:
|
| 569 |
+
return None
|
| 570 |
+
try:
|
| 571 |
+
resp = self._client.get_parameter(Name=name, WithDecryption=with_decryption)
|
| 572 |
+
return (resp.get("Parameter") or {}).get("Value")
|
| 573 |
+
except Exception as e:
|
| 574 |
+
log.warning("ssm get_parameter(%s) failed: %s", name, e)
|
| 575 |
+
return None
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
# ---------------------------------------------------------------------------
|
| 579 |
+
# KMS — small data-key generator for envelope encryption tests
|
| 580 |
+
# ---------------------------------------------------------------------------
|
| 581 |
+
|
| 582 |
+
class KMSDataKeyClient:
|
| 583 |
+
"""Generate data keys to verify the agent's KMS permissions are wired up.
|
| 584 |
+
|
| 585 |
+
Enabled when `KMS_KEY_ID` is set and boto3 creds resolve.
|
| 586 |
+
"""
|
| 587 |
+
|
| 588 |
+
def __init__(self, key_id: Optional[str] = None, region: Optional[str] = None):
|
| 589 |
+
self.key_id = key_id or os.getenv("KMS_KEY_ID")
|
| 590 |
+
self.region = region or os.getenv("AWS_REGION")
|
| 591 |
+
self._client = None
|
| 592 |
+
self.enabled = False
|
| 593 |
+
if self.key_id and _have_aws_credentials():
|
| 594 |
+
try:
|
| 595 |
+
self._client = _client("kms", self.region)
|
| 596 |
+
self._client.describe_key(KeyId=self.key_id)
|
| 597 |
+
self.enabled = True
|
| 598 |
+
log.info("KMS data-key client: %s", self.key_id)
|
| 599 |
+
except Exception as e:
|
| 600 |
+
log.warning("KMS disabled (%s)", e)
|
| 601 |
+
|
| 602 |
+
def generate_data_key(self, spec: str = "AES_256") -> Optional[dict]:
|
| 603 |
+
if not self.enabled or self._client is None:
|
| 604 |
+
return None
|
| 605 |
+
try:
|
| 606 |
+
resp = self._client.generate_data_key(KeyId=self.key_id, KeySpec=spec)
|
| 607 |
+
return {
|
| 608 |
+
"ciphertext_b64_len": len(resp.get("CiphertextBlob", b"")),
|
| 609 |
+
"plaintext_bytes": len(resp.get("Plaintext", b"")),
|
| 610 |
+
"key_id": resp.get("KeyId"),
|
| 611 |
+
}
|
| 612 |
+
except Exception as e:
|
| 613 |
+
log.warning("kms generate_data_key failed: %s", e)
|
| 614 |
+
return None
|
| 615 |
+
|
| 616 |
+
|
| 617 |
# ---------------------------------------------------------------------------
|
| 618 |
# Aggregate status for /aws/status + /dashboard/aws
|
| 619 |
# ---------------------------------------------------------------------------
|
|
|
|
| 645 |
cwm = CloudWatchMetricsPublisher()
|
| 646 |
sns = SNSIncidentPublisher()
|
| 647 |
sec = SecretsLoader()
|
| 648 |
+
sqs = SQSEventPublisher()
|
| 649 |
+
lam = LambdaInvoker()
|
| 650 |
+
eb = EventBridgePublisher()
|
| 651 |
+
xr = XRayTraceReader()
|
| 652 |
+
ssm = SSMParameterReader()
|
| 653 |
+
kms = KMSDataKeyClient()
|
| 654 |
|
| 655 |
_svc("CloudWatch Logs", cw.enabled, {"log_group": cw.log_group})
|
| 656 |
_svc("S3", s3.enabled, {"bucket": s3.bucket, "prefix": s3.prefix})
|
|
|
|
| 658 |
_svc("CloudWatch Metrics",cwm.enabled, {"namespace": cwm.namespace})
|
| 659 |
_svc("SNS", sns.enabled, {"topic": sns.topic_arn})
|
| 660 |
_svc("Secrets Manager", sec.enabled, {"configured_secret": os.getenv("OPENAI_SECRET_ARN")})
|
| 661 |
+
_svc("SQS", sqs.enabled, {"queue": sqs.queue_url})
|
| 662 |
+
_svc("Lambda", lam.enabled, {"function": lam.function_name})
|
| 663 |
+
_svc("EventBridge", eb.enabled, {"bus": eb.bus_name, "source": eb.source})
|
| 664 |
+
_svc("X-Ray", xr.enabled, {"note": "set XRAY_ENABLED=1 to probe"})
|
| 665 |
+
_svc("SSM", ssm.enabled, {"note": "GetParameter access"})
|
| 666 |
+
_svc("KMS", kms.enabled, {"key_id": kms.key_id})
|
| 667 |
_svc("EKS", bool(os.getenv("EKS_CLUSTER_NAME")),
|
| 668 |
{"cluster": os.getenv("EKS_CLUSTER_NAME")})
|
| 669 |
_svc("ECR", bool(os.getenv("ECR_REPOSITORY_URL")),
|
rl-agent/environment/curriculum.py
CHANGED
|
@@ -33,11 +33,14 @@ TIERS: list[str] = [
|
|
| 33 |
]
|
| 34 |
|
| 35 |
TIER_TASKS: dict[str, list[str]] = {
|
| 36 |
-
"warmup": ["task1", "task4", "task9"],
|
| 37 |
-
"beginner": ["task1", "task2", "task4", "task5", "task9"],
|
| 38 |
-
"intermediate": ["task1", "task2", "task3", "task4", "task5", "task6", "task7", "task8",
|
| 39 |
-
|
| 40 |
-
"
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
# Minimum rolling success rate (score >= target) to promote to the next tier.
|
|
|
|
| 33 |
]
|
| 34 |
|
| 35 |
TIER_TASKS: dict[str, list[str]] = {
|
| 36 |
+
"warmup": ["task1", "task4", "task9", "task16", "task19"],
|
| 37 |
+
"beginner": ["task1", "task2", "task4", "task5", "task9", "task16", "task19", "task22"],
|
| 38 |
+
"intermediate": ["task1", "task2", "task3", "task4", "task5", "task6", "task7", "task8",
|
| 39 |
+
"task9", "task10", "task12", "task14", "task17", "task22"],
|
| 40 |
+
"advanced": ["task2", "task3", "task5", "task6", "task7", "task8", "task10", "task11",
|
| 41 |
+
"task12", "task13", "task14", "task15", "task17", "task18", "task20", "task21", "task23"],
|
| 42 |
+
"expert": ["task3", "task6", "task7", "task8", "task10", "task11",
|
| 43 |
+
"task13", "task15", "task18", "task20", "task21", "task23"],
|
| 44 |
}
|
| 45 |
|
| 46 |
# Minimum rolling success rate (score >= target) to promote to the next tier.
|
rl-agent/environment/env.py
CHANGED
|
@@ -46,6 +46,7 @@ from .prometheus_client import PrometheusClient
|
|
| 46 |
logger = logging.getLogger(__name__)
|
| 47 |
|
| 48 |
SCENARIOS_DIR = Path(__file__).resolve().parent.parent / "scenarios"
|
|
|
|
| 49 |
|
| 50 |
TASK_FILE_MAP = {
|
| 51 |
"task1": "easy_redis_exhaustion.json",
|
|
@@ -59,6 +60,19 @@ TASK_FILE_MAP = {
|
|
| 59 |
"task9": "easy_image_pull_backoff.json",
|
| 60 |
"task10": "medium_resource_quota.json",
|
| 61 |
"task11": "hard_liveness_probe_regression.json",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
# Ground truth for reward computation
|
|
@@ -74,6 +88,48 @@ CORRECT_SERVICES = {
|
|
| 74 |
"task9": "checkout-frontend",
|
| 75 |
"task10": "payments-worker",
|
| 76 |
"task11": "inventory-service",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
ALL_AVAILABLE_ACTIONS = [a.value for a in ActionType]
|
|
@@ -136,6 +192,13 @@ class IncidentCommanderEnv:
|
|
| 136 |
self._seen_action_signatures: dict[str, int] = {}
|
| 137 |
self._last_judge_result: dict[str, Any] | None = None
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
# Optional companions (k8s backend, LLM judge, adversarial designer)
|
| 140 |
self._k8s_backend = K8sBackend()
|
| 141 |
self._judge = LLMJudge()
|
|
@@ -186,13 +249,24 @@ class IncidentCommanderEnv:
|
|
| 186 |
"""Fast synchronous reset for mock mode."""
|
| 187 |
if scenario is not None:
|
| 188 |
self._task = scenario
|
|
|
|
| 189 |
else:
|
| 190 |
filename = TASK_FILE_MAP.get(task_id)
|
| 191 |
if not filename:
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
self._step_count = 0
|
| 197 |
self._done = False
|
| 198 |
self._blast_tracker.reset()
|
|
@@ -204,6 +278,15 @@ class IncidentCommanderEnv:
|
|
| 204 |
self._inspected_metrics = False
|
| 205 |
self._inspected_deps = set()
|
| 206 |
self._inspected_traces = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
self._last_reward_breakdown = {}
|
| 208 |
self._last_action_correct = False
|
| 209 |
self._episode_scores = {}
|
|
@@ -212,8 +295,57 @@ class IncidentCommanderEnv:
|
|
| 212 |
self._seen_action_signatures = {}
|
| 213 |
self._last_judge_result = None
|
| 214 |
self._useful_log_query_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
return self._build_mock_observation()
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
def _sync_step(self, action: Action) -> StepResult:
|
| 218 |
"""Fast synchronous step for mock mode."""
|
| 219 |
if self._done:
|
|
@@ -271,9 +403,27 @@ class IncidentCommanderEnv:
|
|
| 271 |
"repeat_count": repeat_count,
|
| 272 |
"judge_score": judge.score,
|
| 273 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
self._step_count += 1
|
| 275 |
done = action.type == ActionType.SUBMIT_POSTMORTEM or self._step_count >= 20
|
| 276 |
self._done = done
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
info = {
|
| 278 |
"reward_breakdown": self._last_reward_breakdown,
|
| 279 |
"action_was_correct": self._last_action_correct,
|
|
@@ -361,6 +511,10 @@ class IncidentCommanderEnv:
|
|
| 361 |
affected_services=p.get("affected_services", []),
|
| 362 |
recommended_followups=p.get("recommended_followups", ""),
|
| 363 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
return f"Unknown action type: {t}"
|
| 365 |
|
| 366 |
def state(self) -> dict[str, Any]:
|
|
@@ -850,6 +1004,9 @@ class IncidentCommanderEnv:
|
|
| 850 |
recommended_followups=p.get("recommended_followups", ""),
|
| 851 |
)
|
| 852 |
else:
|
|
|
|
|
|
|
|
|
|
| 853 |
return f"Unknown action type: {t}"
|
| 854 |
|
| 855 |
async def _action_query_logs(self, service: str, last_minutes: int, filter_text: str | None) -> str:
|
|
@@ -1054,6 +1211,252 @@ class IncidentCommanderEnv:
|
|
| 1054 |
)
|
| 1055 |
return json.dumps({"postmortem_scores": scores}, indent=2)
|
| 1056 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
# ------------------------------------------------------------------
|
| 1058 |
# Reward Function
|
| 1059 |
# ------------------------------------------------------------------
|
|
@@ -1366,6 +1769,383 @@ class IncidentCommanderEnv:
|
|
| 1366 |
if not any(d in cmd for d in (" delete ", " drop ", " --force ")):
|
| 1367 |
breakdown["safe_action_bonus"] = 0.02
|
| 1368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1369 |
total = sum(breakdown.values())
|
| 1370 |
self._last_reward_breakdown = breakdown
|
| 1371 |
self._last_action_correct = breakdown.get("correct_mitigation", 0) > 0
|
|
|
|
| 46 |
logger = logging.getLogger(__name__)
|
| 47 |
|
| 48 |
SCENARIOS_DIR = Path(__file__).resolve().parent.parent / "scenarios"
|
| 49 |
+
SCENARIOS_SIM_DIR = SCENARIOS_DIR / "sim"
|
| 50 |
|
| 51 |
TASK_FILE_MAP = {
|
| 52 |
"task1": "easy_redis_exhaustion.json",
|
|
|
|
| 60 |
"task9": "easy_image_pull_backoff.json",
|
| 61 |
"task10": "medium_resource_quota.json",
|
| 62 |
"task11": "hard_liveness_probe_regression.json",
|
| 63 |
+
# AWS-themed extension tasks (task12–task20).
|
| 64 |
+
"task12": "medium_sqs_dlq_growth.json",
|
| 65 |
+
"task13": "hard_dynamodb_throttle.json",
|
| 66 |
+
"task14": "medium_secrets_manager_rotation.json",
|
| 67 |
+
"task15": "hard_s3_iam_drift.json",
|
| 68 |
+
"task16": "easy_lambda_cold_start.json",
|
| 69 |
+
"task17": "medium_rds_connection_pool.json",
|
| 70 |
+
"task18": "hard_bedrock_throttling.json",
|
| 71 |
+
"task19": "easy_cloudwatch_alarm_storm.json",
|
| 72 |
+
"task20": "hard_eventbridge_silent_drop.json",
|
| 73 |
+
"task21": "hard_step_functions_failure.json",
|
| 74 |
+
"task22": "medium_athena_failed_query.json",
|
| 75 |
+
"task23": "hard_kms_key_drift.json",
|
| 76 |
}
|
| 77 |
|
| 78 |
# Ground truth for reward computation
|
|
|
|
| 88 |
"task9": "checkout-frontend",
|
| 89 |
"task10": "payments-worker",
|
| 90 |
"task11": "inventory-service",
|
| 91 |
+
# New AWS-themed tasks.
|
| 92 |
+
"task12": "order-worker",
|
| 93 |
+
"task13": "inventory-service",
|
| 94 |
+
"task14": "payments-api",
|
| 95 |
+
"task15": "inventory-service",
|
| 96 |
+
"task16": None, # delete_chaos_experiment
|
| 97 |
+
"task17": "payments-api",
|
| 98 |
+
"task18": "notification-service",
|
| 99 |
+
"task19": None, # delete_chaos_experiment
|
| 100 |
+
"task20": "order-worker",
|
| 101 |
+
"task21": "order-worker",
|
| 102 |
+
"task22": "inventory-service",
|
| 103 |
+
"task23": "payments-api",
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
# Per-task AWS service hints — used by aws_evidence_used and security/cost
|
| 107 |
+
# reward signals. Each key lists the AWS terms that a *correct* postmortem
|
| 108 |
+
# would mention. Empty list = task is not AWS-flavoured.
|
| 109 |
+
TASK_AWS_HINTS: dict[str, list[str]] = {
|
| 110 |
+
"task1": [],
|
| 111 |
+
"task2": [],
|
| 112 |
+
"task3": [],
|
| 113 |
+
"task4": [],
|
| 114 |
+
"task5": [],
|
| 115 |
+
"task6": [],
|
| 116 |
+
"task7": [],
|
| 117 |
+
"task8": [],
|
| 118 |
+
"task9": ["ECR", "ImagePullSecret"],
|
| 119 |
+
"task10": [],
|
| 120 |
+
"task11": [],
|
| 121 |
+
"task12": ["SQS", "DLQ", "CloudWatch", "IAM"],
|
| 122 |
+
"task13": ["DynamoDB", "RCU", "WCU", "CloudWatch", "auto-scaling"],
|
| 123 |
+
"task14": ["Secrets Manager", "RotationLambda", "EventBridge", "IAM"],
|
| 124 |
+
"task15": ["S3", "IAM", "AssumeRole", "IRSA", "CloudTrail"],
|
| 125 |
+
"task16": ["Lambda", "Provisioned Concurrency", "CloudWatch"],
|
| 126 |
+
"task17": ["RDS", "PgBouncer", "DatabaseConnections", "IAM"],
|
| 127 |
+
"task18": ["Bedrock", "InvokeModel", "Throttling", "quota", "CloudWatch"],
|
| 128 |
+
"task19": ["CloudWatch", "SNS", "PagerDuty", "PutMetricAlarm"],
|
| 129 |
+
"task20": ["EventBridge", "PutEvents", "DLQ", "IAM", "CloudWatch"],
|
| 130 |
+
"task21": ["Step Functions", "StateMachine", "X-Ray", "IAM", "SNS", "CloudWatch"],
|
| 131 |
+
"task22": ["Athena", "Glue", "S3", "IAM", "CloudWatch"],
|
| 132 |
+
"task23": ["KMS", "CloudTrail", "IAM", "IRSA", "SCP", "Decrypt"],
|
| 133 |
}
|
| 134 |
|
| 135 |
ALL_AVAILABLE_ACTIONS = [a.value for a in ActionType]
|
|
|
|
| 192 |
self._seen_action_signatures: dict[str, int] = {}
|
| 193 |
self._last_judge_result: dict[str, Any] | None = None
|
| 194 |
|
| 195 |
+
# Simulator state — only populated when reset() is called with a
|
| 196 |
+
# `sim_*` task id. Live AWS code paths remain available behind the
|
| 197 |
+
# IC_USE_LIVE_AWS env flag but the simulator is the default.
|
| 198 |
+
self._sim_active: bool = False
|
| 199 |
+
self._sim_state = None
|
| 200 |
+
self._sim_scenario: dict | None = None
|
| 201 |
+
|
| 202 |
# Optional companions (k8s backend, LLM judge, adversarial designer)
|
| 203 |
self._k8s_backend = K8sBackend()
|
| 204 |
self._judge = LLMJudge()
|
|
|
|
| 249 |
"""Fast synchronous reset for mock mode."""
|
| 250 |
if scenario is not None:
|
| 251 |
self._task = scenario
|
| 252 |
+
self._sim_active = False
|
| 253 |
else:
|
| 254 |
filename = TASK_FILE_MAP.get(task_id)
|
| 255 |
if not filename:
|
| 256 |
+
# Sim scenario? task_id is the file stem under scenarios/sim/<difficulty>/.
|
| 257 |
+
sim_path = self._find_sim_scenario(task_id)
|
| 258 |
+
if sim_path is None:
|
| 259 |
+
raise ValueError(f"Unknown task_id: {task_id}. Must be one of {list(TASK_FILE_MAP.keys())} "
|
| 260 |
+
f"or a simulator scenario id (sim_*).")
|
| 261 |
+
self._init_simulator(sim_path)
|
| 262 |
+
# Build a minimal TaskScenario shell so downstream reward
|
| 263 |
+
# plumbing has something to consult.
|
| 264 |
+
self._task = self._task_from_sim(sim_path)
|
| 265 |
+
else:
|
| 266 |
+
scenario_path = SCENARIOS_DIR / filename
|
| 267 |
+
with open(scenario_path) as f:
|
| 268 |
+
self._task = TaskScenario(**json.load(f))
|
| 269 |
+
self._sim_active = False
|
| 270 |
self._step_count = 0
|
| 271 |
self._done = False
|
| 272 |
self._blast_tracker.reset()
|
|
|
|
| 278 |
self._inspected_metrics = False
|
| 279 |
self._inspected_deps = set()
|
| 280 |
self._inspected_traces = False
|
| 281 |
+
# Sim-mode reward bookkeeping (one-shot credits per episode).
|
| 282 |
+
self._seen_sim_inspects: set[str] = set()
|
| 283 |
+
self._sim_mitigation_credited: bool = False
|
| 284 |
+
self._seen_blast: int = 0
|
| 285 |
+
self._sim_trap_credited: bool = False
|
| 286 |
+
self._sim_trolley_credited: bool = False
|
| 287 |
+
self._seen_k8s_kicks: int = 0
|
| 288 |
+
self._aws_inspected: set[str] = set()
|
| 289 |
+
self._aws_remediated: set[str] = set()
|
| 290 |
self._last_reward_breakdown = {}
|
| 291 |
self._last_action_correct = False
|
| 292 |
self._episode_scores = {}
|
|
|
|
| 295 |
self._seen_action_signatures = {}
|
| 296 |
self._last_judge_result = None
|
| 297 |
self._useful_log_query_count = 0
|
| 298 |
+
# AWS-flavoured action tracking
|
| 299 |
+
self._aws_inspected = set()
|
| 300 |
+
self._aws_remediated = set()
|
| 301 |
+
# Simulator state is owned by _init_simulator() when applicable; clear
|
| 302 |
+
# it here for non-sim resets so a previous sim run doesn't leak.
|
| 303 |
+
if not getattr(self, "_sim_active", False):
|
| 304 |
+
self._sim_state = None
|
| 305 |
+
self._sim_scenario = None
|
| 306 |
return self._build_mock_observation()
|
| 307 |
|
| 308 |
+
# ------------------------------------------------------------------
|
| 309 |
+
# Simulator integration
|
| 310 |
+
# ------------------------------------------------------------------
|
| 311 |
+
def _find_sim_scenario(self, task_id: str) -> Path | None:
|
| 312 |
+
"""Locate a sim scenario by id (filename stem) under scenarios/sim/."""
|
| 313 |
+
if not SCENARIOS_SIM_DIR.exists():
|
| 314 |
+
return None
|
| 315 |
+
for p in SCENARIOS_SIM_DIR.rglob(f"{task_id}.json"):
|
| 316 |
+
return p
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
def _init_simulator(self, scenario_path: Path) -> None:
|
| 320 |
+
"""Spin up a fresh SimState and load the scenario."""
|
| 321 |
+
# Local import: keeps the simulator package optional.
|
| 322 |
+
from simulator import SimState, load_scenario
|
| 323 |
+
self._sim_state = SimState()
|
| 324 |
+
self._sim_scenario = load_scenario(scenario_path, self._sim_state)
|
| 325 |
+
self._sim_active = True
|
| 326 |
+
|
| 327 |
+
def _task_from_sim(self, scenario_path: Path) -> TaskScenario:
|
| 328 |
+
"""Synthesise a TaskScenario shell from a sim scenario JSON.
|
| 329 |
+
|
| 330 |
+
The shell is just enough for the reward & observation builders to run;
|
| 331 |
+
actual work happens via the simulator.
|
| 332 |
+
"""
|
| 333 |
+
scn = json.loads(scenario_path.read_text(encoding="utf-8"))
|
| 334 |
+
difficulty_str = scn.get("difficulty", "medium")
|
| 335 |
+
# TaskScenario has many required fields; fill them with sim-friendly
|
| 336 |
+
# defaults. These are not consulted by the simulator path, but the
|
| 337 |
+
# reward plumbing reads `difficulty` and `target_score`.
|
| 338 |
+
return TaskScenario(
|
| 339 |
+
task_id=scn["id"],
|
| 340 |
+
difficulty=difficulty_str,
|
| 341 |
+
target_score=scn.get("target_score", 0.55),
|
| 342 |
+
fault_type="simulated",
|
| 343 |
+
ground_truth_root_cause=scn.get("description", scn.get("title", "")),
|
| 344 |
+
correct_mitigation_action="aws_api_call",
|
| 345 |
+
correct_mitigation_target=scn["correct_action_chain"][-1]["id"]
|
| 346 |
+
if scn.get("correct_action_chain") else "",
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
def _sync_step(self, action: Action) -> StepResult:
|
| 350 |
"""Fast synchronous step for mock mode."""
|
| 351 |
if self._done:
|
|
|
|
| 403 |
"repeat_count": repeat_count,
|
| 404 |
"judge_score": judge.score,
|
| 405 |
})
|
| 406 |
+
|
| 407 |
+
# ── Phase 8: replay recording (sim mode only). ──
|
| 408 |
+
if self._sim_active and self._sim_state is not None:
|
| 409 |
+
from .replay import record_step
|
| 410 |
+
record_step(self._sim_state,
|
| 411 |
+
action={"id": f"{action.type.value}",
|
| 412 |
+
"params": action.params},
|
| 413 |
+
action_result=str(action_result),
|
| 414 |
+
step_reward=reward)
|
| 415 |
+
|
| 416 |
self._step_count += 1
|
| 417 |
done = action.type == ActionType.SUBMIT_POSTMORTEM or self._step_count >= 20
|
| 418 |
self._done = done
|
| 419 |
+
|
| 420 |
+
# ── Phase 8: dump replay artifact at episode end. ──
|
| 421 |
+
if done and self._sim_active and self._sim_state is not None:
|
| 422 |
+
try:
|
| 423 |
+
self._dump_replay()
|
| 424 |
+
except Exception as exc: # noqa: BLE001
|
| 425 |
+
logger.warning("replay dump failed: %s", exc)
|
| 426 |
+
|
| 427 |
info = {
|
| 428 |
"reward_breakdown": self._last_reward_breakdown,
|
| 429 |
"action_was_correct": self._last_action_correct,
|
|
|
|
| 511 |
affected_services=p.get("affected_services", []),
|
| 512 |
recommended_followups=p.get("recommended_followups", ""),
|
| 513 |
)
|
| 514 |
+
# ---- AWS-flavoured non-obvious actions (sync = same impl) ----
|
| 515 |
+
aws_result = self._dispatch_aws_action(action)
|
| 516 |
+
if aws_result is not None:
|
| 517 |
+
return aws_result
|
| 518 |
return f"Unknown action type: {t}"
|
| 519 |
|
| 520 |
def state(self) -> dict[str, Any]:
|
|
|
|
| 1004 |
recommended_followups=p.get("recommended_followups", ""),
|
| 1005 |
)
|
| 1006 |
else:
|
| 1007 |
+
aws_result = self._dispatch_aws_action(action)
|
| 1008 |
+
if aws_result is not None:
|
| 1009 |
+
return aws_result
|
| 1010 |
return f"Unknown action type: {t}"
|
| 1011 |
|
| 1012 |
async def _action_query_logs(self, service: str, last_minutes: int, filter_text: str | None) -> str:
|
|
|
|
| 1211 |
)
|
| 1212 |
return json.dumps({"postmortem_scores": scores}, indent=2)
|
| 1213 |
|
| 1214 |
+
# ------------------------------------------------------------------
|
| 1215 |
+
# AWS-flavoured non-obvious action dispatch
|
| 1216 |
+
# ------------------------------------------------------------------
|
| 1217 |
+
|
| 1218 |
+
def _dispatch_aws_action(self, action: Action) -> str | None:
|
| 1219 |
+
"""Run an AWS-flavoured action. Returns None if the action type is
|
| 1220 |
+
not one of the AWS variants (caller falls through to 'Unknown').
|
| 1221 |
+
|
| 1222 |
+
Side-effects on env state:
|
| 1223 |
+
* tracks which forensic tools have been used (rewards consult these)
|
| 1224 |
+
* marks `_mitigation_applied` for the AWS-write actions when the
|
| 1225 |
+
target matches the task's correct_mitigation_target.
|
| 1226 |
+
"""
|
| 1227 |
+
from . import aws_actions as A
|
| 1228 |
+
t = action.type
|
| 1229 |
+
p = action.params or {}
|
| 1230 |
+
# Lazily-initialised investigation sets.
|
| 1231 |
+
self._aws_inspected = getattr(self, "_aws_inspected", set())
|
| 1232 |
+
self._aws_remediated = getattr(self, "_aws_remediated", set())
|
| 1233 |
+
|
| 1234 |
+
# If a simulator scenario is active, every AWS action goes through
|
| 1235 |
+
# the simulator engine so live AWS is never touched.
|
| 1236 |
+
if getattr(self, "_sim_active", False) and self._sim_state is not None:
|
| 1237 |
+
sim_result = self._dispatch_sim_action(action)
|
| 1238 |
+
if sim_result is not None:
|
| 1239 |
+
return sim_result
|
| 1240 |
+
|
| 1241 |
+
# ---------- read / forensic ----------
|
| 1242 |
+
if t == ActionType.CHECK_CLOUDTRAIL_EVENTS:
|
| 1243 |
+
self._aws_inspected.add("cloudtrail")
|
| 1244 |
+
return A.check_cloudtrail_events(
|
| 1245 |
+
resource_name=p.get("resource_name", ""),
|
| 1246 |
+
event_name=p.get("event_name", ""),
|
| 1247 |
+
last_minutes=int(p.get("last_minutes", 60)))
|
| 1248 |
+
if t == ActionType.DESCRIBE_RESOURCE_POLICY:
|
| 1249 |
+
self._aws_inspected.add("resource_policy")
|
| 1250 |
+
return A.describe_resource_policy(target=p.get("target", ""))
|
| 1251 |
+
if t == ActionType.GET_QUOTA_USAGE:
|
| 1252 |
+
self._aws_inspected.add("quota")
|
| 1253 |
+
return A.get_quota_usage(
|
| 1254 |
+
service_code=p.get("service_code", ""),
|
| 1255 |
+
quota_code=p.get("quota_code", ""))
|
| 1256 |
+
if t == ActionType.CHECK_SECRET_ROTATION:
|
| 1257 |
+
self._aws_inspected.add("secret_rotation")
|
| 1258 |
+
return A.check_secret_rotation(
|
| 1259 |
+
secret_name=p.get("secret_name", ""))
|
| 1260 |
+
if t == ActionType.VALIDATE_IAM_PERMISSION:
|
| 1261 |
+
self._aws_inspected.add("iam_sim")
|
| 1262 |
+
return A.validate_iam_permission(
|
| 1263 |
+
action=p.get("iam_action", ""),
|
| 1264 |
+
resource=p.get("resource", "*"),
|
| 1265 |
+
principal=p.get("principal", ""))
|
| 1266 |
+
if t == ActionType.ANALYZE_CLOUDWATCH_INSIGHTS:
|
| 1267 |
+
self._aws_inspected.add("insights")
|
| 1268 |
+
return A.analyze_cloudwatch_insights(
|
| 1269 |
+
log_group=p.get("log_group", ""),
|
| 1270 |
+
pattern=p.get("pattern", "ERROR"),
|
| 1271 |
+
last_minutes=int(p.get("last_minutes", 15)))
|
| 1272 |
+
if t == ActionType.INSPECT_DLQ_MESSAGES:
|
| 1273 |
+
self._aws_inspected.add("dlq_inspect")
|
| 1274 |
+
return A.inspect_dlq_messages(
|
| 1275 |
+
queue_name=p.get("queue_name", "ic-orders-dlq"),
|
| 1276 |
+
max_messages=int(p.get("max_messages", 5)))
|
| 1277 |
+
if t == ActionType.DIFF_CONFIG_VERSIONS:
|
| 1278 |
+
self._aws_inspected.add("ssm_diff")
|
| 1279 |
+
return A.diff_config_versions(
|
| 1280 |
+
parameter_name=p.get("parameter_name", ""),
|
| 1281 |
+
a=int(p.get("a", 0)), b=int(p.get("b", 0)))
|
| 1282 |
+
if t == ActionType.DESCRIBE_STATE_MACHINE_EXEC:
|
| 1283 |
+
self._aws_inspected.add("sfn_exec")
|
| 1284 |
+
return A.describe_state_machine_execution(
|
| 1285 |
+
execution_arn=p.get("execution_arn", ""),
|
| 1286 |
+
state_machine_name=p.get("state_machine_name", ""))
|
| 1287 |
+
|
| 1288 |
+
# ---------- write / remediation ----------
|
| 1289 |
+
if t == ActionType.INVOKE_LAMBDA:
|
| 1290 |
+
self._aws_remediated.add("lambda")
|
| 1291 |
+
# Recognise correct mitigation when the agent invokes the runbook
|
| 1292 |
+
# lambda for a task whose correct action is invoke_lambda.
|
| 1293 |
+
if (self._task and
|
| 1294 |
+
self._task.correct_mitigation_action == "invoke_lambda"):
|
| 1295 |
+
self._mitigation_applied = True
|
| 1296 |
+
return A.invoke_lambda(
|
| 1297 |
+
function_name=p.get("function_name", ""),
|
| 1298 |
+
payload=p.get("payload"))
|
| 1299 |
+
if t == ActionType.ROTATE_SECRET:
|
| 1300 |
+
self._aws_remediated.add("secret_rotation")
|
| 1301 |
+
if (self._task and
|
| 1302 |
+
self._task.correct_mitigation_action == "rotate_secret"):
|
| 1303 |
+
self._mitigation_applied = True
|
| 1304 |
+
return A.rotate_secret(
|
| 1305 |
+
secret_name=p.get("secret_name", ""))
|
| 1306 |
+
if t == ActionType.PURGE_QUEUE:
|
| 1307 |
+
self._aws_remediated.add("purge")
|
| 1308 |
+
if (self._task and
|
| 1309 |
+
self._task.correct_mitigation_action == "purge_queue"):
|
| 1310 |
+
self._mitigation_applied = True
|
| 1311 |
+
return A.purge_queue(queue_name=p.get("queue_name", ""))
|
| 1312 |
+
if t == ActionType.ENABLE_EVENTBRIDGE_RULE:
|
| 1313 |
+
self._aws_remediated.add("enable_rule")
|
| 1314 |
+
if (self._task and
|
| 1315 |
+
self._task.correct_mitigation_action == "enable_eventbridge_rule"):
|
| 1316 |
+
self._mitigation_applied = True
|
| 1317 |
+
return A.enable_eventbridge_rule(
|
| 1318 |
+
rule_name=p.get("rule_name", ""),
|
| 1319 |
+
bus_name=p.get("bus_name", ""))
|
| 1320 |
+
return None
|
| 1321 |
+
|
| 1322 |
+
# ------------------------------------------------------------------
|
| 1323 |
+
# Simulator dispatch
|
| 1324 |
+
# ------------------------------------------------------------------
|
| 1325 |
+
# Map ActionType variants (the "named" AWS actions) to a (service, verb)
|
| 1326 |
+
# tuple plus a parameter projector. The simulator engine just consumes
|
| 1327 |
+
# `{"id": "<service>.<verb>", "params": {...}}`, so we translate.
|
| 1328 |
+
_AWS_ACTION_MAP: dict = {
|
| 1329 |
+
# Read / forensic
|
| 1330 |
+
ActionType.CHECK_CLOUDTRAIL_EVENTS:
|
| 1331 |
+
("cloudtrail", "list_events", lambda p: {
|
| 1332 |
+
"filter": p.get("event_name", ""),
|
| 1333 |
+
"range_minutes": int(p.get("last_minutes", 60))}),
|
| 1334 |
+
ActionType.DESCRIBE_RESOURCE_POLICY:
|
| 1335 |
+
("s3", "get_policy", lambda p: {"resource_id": p.get("target", "")}),
|
| 1336 |
+
ActionType.GET_QUOTA_USAGE:
|
| 1337 |
+
("cloudwatch", "get_quota", lambda p: {
|
| 1338 |
+
"service_code": p.get("service_code", "")}),
|
| 1339 |
+
ActionType.CHECK_SECRET_ROTATION:
|
| 1340 |
+
("secretsmanager", "describe", lambda p: {
|
| 1341 |
+
"resource_id": p.get("secret_name", "")}),
|
| 1342 |
+
ActionType.VALIDATE_IAM_PERMISSION:
|
| 1343 |
+
("iam", "simulate_policy", lambda p: {
|
| 1344 |
+
"principal": p.get("principal", ""),
|
| 1345 |
+
"action_name": p.get("iam_action", ""),
|
| 1346 |
+
"resource": p.get("resource", "*")}),
|
| 1347 |
+
ActionType.ANALYZE_CLOUDWATCH_INSIGHTS:
|
| 1348 |
+
("cloudwatch", "get_logs", lambda p: {
|
| 1349 |
+
"log_group": p.get("log_group", ""),
|
| 1350 |
+
"filter": p.get("pattern", ""),
|
| 1351 |
+
"range_minutes": int(p.get("last_minutes", 15))}),
|
| 1352 |
+
ActionType.INSPECT_DLQ_MESSAGES:
|
| 1353 |
+
("sqs", "receive", lambda p: {
|
| 1354 |
+
"resource_id": p.get("queue_name", ""),
|
| 1355 |
+
"limit": int(p.get("max_messages", 5))}),
|
| 1356 |
+
ActionType.DIFF_CONFIG_VERSIONS:
|
| 1357 |
+
("ssm", "diff_versions", lambda p: {
|
| 1358 |
+
"resource_id": p.get("parameter_name", ""),
|
| 1359 |
+
"v1": p.get("v1", 1), "v2": p.get("v2", 2)}),
|
| 1360 |
+
ActionType.DESCRIBE_STATE_MACHINE_EXEC:
|
| 1361 |
+
("stepfunctions", "describe", lambda p: {
|
| 1362 |
+
"resource_id": p.get("state_machine_name", "")}),
|
| 1363 |
+
# Write / remediation
|
| 1364 |
+
ActionType.INVOKE_LAMBDA:
|
| 1365 |
+
("lambda", "invoke", lambda p: {
|
| 1366 |
+
"resource_id": p.get("function_name", ""),
|
| 1367 |
+
"payload": p.get("payload", "")}),
|
| 1368 |
+
ActionType.ROTATE_SECRET:
|
| 1369 |
+
("secretsmanager", "rotate", lambda p: {
|
| 1370 |
+
"resource_id": p.get("secret_name", "")}),
|
| 1371 |
+
ActionType.PURGE_QUEUE:
|
| 1372 |
+
("sqs", "purge", lambda p: {
|
| 1373 |
+
"resource_id": p.get("queue_name", "")}),
|
| 1374 |
+
ActionType.ENABLE_EVENTBRIDGE_RULE:
|
| 1375 |
+
("events", "enable", lambda p: {
|
| 1376 |
+
"bus_name": p.get("bus_name", ""),
|
| 1377 |
+
"rule_name": p.get("rule_name", "")}),
|
| 1378 |
+
}
|
| 1379 |
+
|
| 1380 |
+
def _dispatch_sim_action(self, action: Action) -> str | None:
|
| 1381 |
+
"""Route the action through the simulator. Returns a string result, or
|
| 1382 |
+
None if this action variant has no simulator mapping (caller falls
|
| 1383 |
+
back to live-AWS code paths — which are themselves no-ops when
|
| 1384 |
+
IC_USE_LIVE_AWS is unset)."""
|
| 1385 |
+
from simulator import dispatch as sim_dispatch
|
| 1386 |
+
p = action.params or {}
|
| 1387 |
+
|
| 1388 |
+
# Generic AWS_API_CALL — agent specifies service + verb directly.
|
| 1389 |
+
if action.type == ActionType.AWS_API_CALL:
|
| 1390 |
+
service = p.get("service")
|
| 1391 |
+
verb = p.get("verb")
|
| 1392 |
+
if not service or not verb:
|
| 1393 |
+
return "[sim:error] AWS_API_CALL requires params.service and params.verb"
|
| 1394 |
+
inner_params = {k: v for k, v in p.items()
|
| 1395 |
+
if k not in ("service", "verb")}
|
| 1396 |
+
result = sim_dispatch(
|
| 1397 |
+
{"id": f"{service}.{verb}", "params": inner_params},
|
| 1398 |
+
self._sim_state)
|
| 1399 |
+
self._sync_sim_breadcrumbs(result)
|
| 1400 |
+
self._tag_breadcrumbs(result, service, verb)
|
| 1401 |
+
return result.as_observation()
|
| 1402 |
+
|
| 1403 |
+
mapping = self._AWS_ACTION_MAP.get(action.type)
|
| 1404 |
+
if mapping is None:
|
| 1405 |
+
return None
|
| 1406 |
+
service, verb, project = mapping
|
| 1407 |
+
result = sim_dispatch(
|
| 1408 |
+
{"id": f"{service}.{verb}", "params": project(p)},
|
| 1409 |
+
self._sim_state)
|
| 1410 |
+
self._sync_sim_breadcrumbs(result)
|
| 1411 |
+
self._tag_breadcrumbs(result, service, verb)
|
| 1412 |
+
return result.as_observation()
|
| 1413 |
+
|
| 1414 |
+
def _tag_breadcrumbs(self, result, service: str, verb: str) -> None:
|
| 1415 |
+
"""Translate simulator tags into env reward breadcrumbs."""
|
| 1416 |
+
# Inspection-flavoured tags from both legacy AWS handlers and the
|
| 1417 |
+
# `platform.*` physics handlers.
|
| 1418 |
+
INSPECT_TAGS = {
|
| 1419 |
+
"cloudtrail", "iam_sim", "insights", "dlq_inspect",
|
| 1420 |
+
"secret_rotation", "ssm_diff", "resource_policy", "sfn_exec",
|
| 1421 |
+
"investigation", "logs", "trace", "metrics", "topology",
|
| 1422 |
+
"runbook_read", "runbook_search", "heap_dump", "traffic",
|
| 1423 |
+
"slack",
|
| 1424 |
+
}
|
| 1425 |
+
for tag in result.tags:
|
| 1426 |
+
if tag in INSPECT_TAGS:
|
| 1427 |
+
self._aws_inspected.add(tag)
|
| 1428 |
+
if tag in ("remediation", "remediation_safe", "remediation_pending"):
|
| 1429 |
+
self._aws_remediated.add(f"{service}.{verb}")
|
| 1430 |
+
|
| 1431 |
+
def _sync_sim_breadcrumbs(self, result) -> None:
|
| 1432 |
+
"""Fold simulator-side breadcrumbs into env reward state."""
|
| 1433 |
+
if not result.ok:
|
| 1434 |
+
return
|
| 1435 |
+
if self._sim_state is not None and self._sim_state.mitigation_applied:
|
| 1436 |
+
self._mitigation_applied = True
|
| 1437 |
+
|
| 1438 |
+
def _dump_replay(self) -> Path | None:
|
| 1439 |
+
"""Write a standalone HTML replay artifact next to checkpoints/.
|
| 1440 |
+
|
| 1441 |
+
Path: `<repo>/rl-agent/replays/{task_id}_{episode}.html`. The path is
|
| 1442 |
+
configurable via env var `IC_REPLAY_DIR`.
|
| 1443 |
+
"""
|
| 1444 |
+
if self._sim_state is None or self._task is None:
|
| 1445 |
+
return None
|
| 1446 |
+
from .replay import build_replay
|
| 1447 |
+
out_dir = Path(os.environ.get("IC_REPLAY_DIR",
|
| 1448 |
+
Path(__file__).resolve().parent.parent
|
| 1449 |
+
/ "replays"))
|
| 1450 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 1451 |
+
ep_id = f"{self._task.task_id}_{int(self._sim_state.clock_s)}"
|
| 1452 |
+
out = out_dir / f"{ep_id}.html"
|
| 1453 |
+
return build_replay(self._sim_state,
|
| 1454 |
+
task_id=self._task.task_id,
|
| 1455 |
+
episode_id=ep_id,
|
| 1456 |
+
final_reward=self._cumulative_reward,
|
| 1457 |
+
mitigated=self._mitigation_applied,
|
| 1458 |
+
out_path=out)
|
| 1459 |
+
|
| 1460 |
# ------------------------------------------------------------------
|
| 1461 |
# Reward Function
|
| 1462 |
# ------------------------------------------------------------------
|
|
|
|
| 1769 |
if not any(d in cmd for d in (" delete ", " drop ", " --force ")):
|
| 1770 |
breakdown["safe_action_bonus"] = 0.02
|
| 1771 |
|
| 1772 |
+
# =============================================================
|
| 1773 |
+
# AWS-aware + safety + observability reward signals (round 3).
|
| 1774 |
+
# All deterministic, low-magnitude, gated on action+task context.
|
| 1775 |
+
# =============================================================
|
| 1776 |
+
|
| 1777 |
+
aws_hints = TASK_AWS_HINTS.get(task_id, [])
|
| 1778 |
+
# Lowercased postmortem text once, used by several signals below.
|
| 1779 |
+
pm_blob = ""
|
| 1780 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1781 |
+
pm_blob = " ".join(
|
| 1782 |
+
str(v) for v in action.params.values() if isinstance(v, (str, list))
|
| 1783 |
+
).lower()
|
| 1784 |
+
|
| 1785 |
+
# --- aws_evidence_used (+0.06): postmortem mentions \u22651 expected AWS term ---
|
| 1786 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM and aws_hints:
|
| 1787 |
+
hits = sum(1 for h in aws_hints if h.lower() in pm_blob)
|
| 1788 |
+
if hits >= 1:
|
| 1789 |
+
breakdown["aws_evidence_used"] = round(min(0.06, 0.02 * hits), 3)
|
| 1790 |
+
|
| 1791 |
+
# --- iam_least_privilege_bonus (+0.05): postmortem mentions IAM scoping ---
|
| 1792 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1793 |
+
if any(kw in pm_blob for kw in (
|
| 1794 |
+
"least-privilege", "least privilege", "irsa",
|
| 1795 |
+
"iam policy", "scope", "assumerole")):
|
| 1796 |
+
breakdown["iam_least_privilege_bonus"] = 0.05
|
| 1797 |
+
|
| 1798 |
+
# --- cost_awareness_bonus (+0.03): postmortem reasons about cost ---
|
| 1799 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1800 |
+
if any(kw in pm_blob for kw in (
|
| 1801 |
+
"cost", "budget", "$", "cheaper", "pay_per_request",
|
| 1802 |
+
"provisioned concurrency", "rcu", "wcu")):
|
| 1803 |
+
breakdown["cost_awareness_bonus"] = 0.03
|
| 1804 |
+
|
| 1805 |
+
# --- security_followup_bonus (+0.04): a followup mentions security ---
|
| 1806 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1807 |
+
fu = action.params.get("recommended_followups", []) or []
|
| 1808 |
+
joined = " ".join(str(x) for x in fu).lower() if isinstance(fu, list) else ""
|
| 1809 |
+
if any(k in joined for k in ("rotate", "audit", "rbac", "iam",
|
| 1810 |
+
"secret", "ciphertext", "encrypt")):
|
| 1811 |
+
breakdown["security_followup_bonus"] = 0.04
|
| 1812 |
+
|
| 1813 |
+
# --- timeline_quality_bonus (+0.04): timeline has \u22653 dated entries ---
|
| 1814 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1815 |
+
timeline = action.params.get("timeline", "")
|
| 1816 |
+
if isinstance(timeline, str):
|
| 1817 |
+
# Crude: count occurrences of T+ markers or HH:MM-style stamps.
|
| 1818 |
+
ts_count = len(re.findall(r"(?:T\+\d|\d{1,2}:\d{2})", timeline))
|
| 1819 |
+
if ts_count >= 3:
|
| 1820 |
+
breakdown["timeline_quality_bonus"] = 0.04
|
| 1821 |
+
|
| 1822 |
+
# --- replica_target_safe (+0.02): scale to a sane replica count ---
|
| 1823 |
+
if action.type == ActionType.SCALE_DEPLOYMENT:
|
| 1824 |
+
try:
|
| 1825 |
+
n = int(action.params.get("replicas", 0))
|
| 1826 |
+
if 1 <= n <= 20:
|
| 1827 |
+
breakdown["replica_target_safe"] = 0.02
|
| 1828 |
+
elif n > 50:
|
| 1829 |
+
breakdown["replica_target_unsafe"] = -0.05
|
| 1830 |
+
except (TypeError, ValueError):
|
| 1831 |
+
pass
|
| 1832 |
+
|
| 1833 |
+
# --- correct_action_type (+0.05): right action verb even if wrong target ---
|
| 1834 |
+
if (action.type in WRITE_ACTIONS
|
| 1835 |
+
and action.type.value == task.correct_mitigation_action
|
| 1836 |
+
and not self._mitigation_applied):
|
| 1837 |
+
breakdown["correct_action_type"] = 0.05
|
| 1838 |
+
|
| 1839 |
+
# --- service_dependency_traversal (+0.04): queried logs of a known dep ---
|
| 1840 |
+
if action.type == ActionType.QUERY_LOGS:
|
| 1841 |
+
svc = action.params.get("service", "")
|
| 1842 |
+
correct = CORRECT_SERVICES.get(task_id) or ""
|
| 1843 |
+
deps = SERVICE_DEPENDENCIES.get(correct, [])
|
| 1844 |
+
if svc and svc in deps and "service_dependency_traversal" not in breakdown:
|
| 1845 |
+
breakdown["service_dependency_traversal"] = 0.04
|
| 1846 |
+
|
| 1847 |
+
# --- alert_triage_match (+0.05): first read targets a service in alerts ---
|
| 1848 |
+
if (self._step_count == 1
|
| 1849 |
+
and action.type in {ActionType.QUERY_LOGS,
|
| 1850 |
+
ActionType.QUERY_METRICS,
|
| 1851 |
+
ActionType.GET_SERVICE_DEPENDENCIES}
|
| 1852 |
+
and obs.active_alerts):
|
| 1853 |
+
alert_svcs = {a.service for a in obs.active_alerts}
|
| 1854 |
+
tgt = action.params.get("service", "") or str(action.params.get("promql", ""))
|
| 1855 |
+
if any(s and s in tgt for s in alert_svcs):
|
| 1856 |
+
breakdown["alert_triage_match"] = 0.05
|
| 1857 |
+
|
| 1858 |
+
# --- progressive_investigation (+0.04): logs \u2192 metrics \u2192 trace order ---
|
| 1859 |
+
if action.type == ActionType.GET_TRACE:
|
| 1860 |
+
seen_logs = any(a["action_type"] == "query_logs"
|
| 1861 |
+
for a in self._action_history)
|
| 1862 |
+
seen_metrics = any(a["action_type"] == "query_metrics"
|
| 1863 |
+
for a in self._action_history)
|
| 1864 |
+
if seen_logs and seen_metrics and not self._inspected_traces:
|
| 1865 |
+
breakdown["progressive_investigation"] = 0.04
|
| 1866 |
+
|
| 1867 |
+
# --- specific_promql_bonus (+0.03): query uses rate()/histogram_quantile ---
|
| 1868 |
+
if action.type == ActionType.QUERY_METRICS:
|
| 1869 |
+
promql = str(action.params.get("promql", "")).lower()
|
| 1870 |
+
if any(fn in promql for fn in ("rate(", "histogram_quantile",
|
| 1871 |
+
"increase(", "sum by", "max_over_time")):
|
| 1872 |
+
breakdown["specific_promql_bonus"] = 0.03
|
| 1873 |
+
|
| 1874 |
+
# --- avoided_red_herring (+0.05): never wrote to a red-herring service ---
|
| 1875 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM and task.red_herrings:
|
| 1876 |
+
wrote_herring = any(
|
| 1877 |
+
a.get("action_type") in {w.value for w in WRITE_ACTIONS}
|
| 1878 |
+
and any(h.lower() in str(a.get("params", {}).get("deployment", "")).lower()
|
| 1879 |
+
for h in task.red_herrings)
|
| 1880 |
+
for a in self._action_history
|
| 1881 |
+
)
|
| 1882 |
+
if not wrote_herring:
|
| 1883 |
+
breakdown["avoided_red_herring"] = 0.05
|
| 1884 |
+
|
| 1885 |
+
# --- evidence_diversity_bonus (+0.05): \u22654 distinct evidence sources ---
|
| 1886 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1887 |
+
n_src = (
|
| 1888 |
+
int(bool(self._inspected_logs))
|
| 1889 |
+
+ int(self._inspected_metrics)
|
| 1890 |
+
+ int(bool(self._inspected_deps))
|
| 1891 |
+
+ int(self._inspected_traces)
|
| 1892 |
+
+ int(any(k.startswith("exec:") for k in self._inspected_logs))
|
| 1893 |
+
)
|
| 1894 |
+
if n_src >= 4:
|
| 1895 |
+
breakdown["evidence_diversity_bonus"] = 0.05
|
| 1896 |
+
|
| 1897 |
+
# --- mitigation_target_match (+0.04): write target equals correct service ---
|
| 1898 |
+
correct_for_task = CORRECT_SERVICES.get(task_id)
|
| 1899 |
+
if (correct_for_task
|
| 1900 |
+
and action.type in WRITE_ACTIONS
|
| 1901 |
+
and action.type != ActionType.DELETE_CHAOS_EXPERIMENT):
|
| 1902 |
+
target_deploy = action.params.get("deployment", "")
|
| 1903 |
+
if target_deploy == correct_for_task:
|
| 1904 |
+
breakdown["mitigation_target_match"] = 0.04
|
| 1905 |
+
|
| 1906 |
+
# --- chaos_detection_bonus (+0.04): probed chaos for a chaos task ---
|
| 1907 |
+
if (task.chaos_experiment_name
|
| 1908 |
+
and action.type == ActionType.EXEC_KUBECTL):
|
| 1909 |
+
cmd = str(action.params.get("command", "")).lower()
|
| 1910 |
+
if "chaos" in cmd or "podchaos" in cmd or "networkchaos" in cmd:
|
| 1911 |
+
breakdown["chaos_detection_bonus"] = 0.04
|
| 1912 |
+
|
| 1913 |
+
# --- timely_acknowledgment (+0.03): first action within 2 steps ---
|
| 1914 |
+
if self._step_count <= 1 and action.type in READ_ACTIONS:
|
| 1915 |
+
breakdown["timely_acknowledgment"] = 0.03
|
| 1916 |
+
|
| 1917 |
+
# --- summary_brevity_penalty (-0.02): postmortem summary is empty ---
|
| 1918 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 1919 |
+
summary_text = str(action.params.get("summary", "")).strip()
|
| 1920 |
+
if len(summary_text) < 20:
|
| 1921 |
+
breakdown["summary_brevity_penalty"] = -0.02
|
| 1922 |
+
|
| 1923 |
+
# =============================================================
|
| 1924 |
+
# ROUND-4 REWARDS \u2014 dense, non-obvious AWS-aware signals.
|
| 1925 |
+
# Goal: push the actor to PICK THE RIGHT FORENSIC TOOL FOR THE
|
| 1926 |
+
# SYMPTOMS rather than fall back on logs+kubectl every time.
|
| 1927 |
+
# All values are deterministic and small (\u22640.10).
|
| 1928 |
+
# =============================================================
|
| 1929 |
+
|
| 1930 |
+
aws_inspected = getattr(self, "_aws_inspected", set())
|
| 1931 |
+
aws_remediated = getattr(self, "_aws_remediated", set())
|
| 1932 |
+
|
| 1933 |
+
# Tasks where each forensic tool yields the *non-obvious* clue.
|
| 1934 |
+
# These are intentionally not 1:1 with task ids \u2014 the agent has
|
| 1935 |
+
# to learn the mapping by trial.
|
| 1936 |
+
cloudtrail_tasks = {"task14", "task15", "task19", "task20",
|
| 1937 |
+
"task23"}
|
| 1938 |
+
resource_policy_tasks = {"task14", "task15", "task20", "task23"}
|
| 1939 |
+
quota_tasks = {"task13", "task16", "task18"}
|
| 1940 |
+
secret_rotation_tasks = {"task14", "task23"}
|
| 1941 |
+
iam_sim_tasks = {"task15", "task20", "task23"}
|
| 1942 |
+
insights_tasks = {"task9", "task12", "task16", "task17",
|
| 1943 |
+
"task22"}
|
| 1944 |
+
dlq_inspect_tasks = {"task12", "task20"}
|
| 1945 |
+
ssm_diff_tasks = {"task17", "task7"}
|
| 1946 |
+
sfn_exec_tasks = {"task21"}
|
| 1947 |
+
|
| 1948 |
+
# ---- 1. cloudtrail_for_drift_task (+0.07) ------------------------
|
| 1949 |
+
# Used cloudtrail on a task whose root cause is an operator action.
|
| 1950 |
+
if (action.type == ActionType.CHECK_CLOUDTRAIL_EVENTS
|
| 1951 |
+
and task_id in cloudtrail_tasks
|
| 1952 |
+
and "cloudtrail_first_use" not in breakdown):
|
| 1953 |
+
breakdown["cloudtrail_for_drift_task"] = 0.07
|
| 1954 |
+
# ---- 2. cloudtrail_irrelevant_penalty (-0.03) -------------------
|
| 1955 |
+
elif (action.type == ActionType.CHECK_CLOUDTRAIL_EVENTS
|
| 1956 |
+
and task_id not in cloudtrail_tasks):
|
| 1957 |
+
breakdown["cloudtrail_irrelevant_penalty"] = -0.03
|
| 1958 |
+
|
| 1959 |
+
# ---- 3. policy_inspection_for_drift (+0.06) ---------------------
|
| 1960 |
+
if (action.type == ActionType.DESCRIBE_RESOURCE_POLICY
|
| 1961 |
+
and task_id in resource_policy_tasks):
|
| 1962 |
+
breakdown["policy_inspection_for_drift"] = 0.06
|
| 1963 |
+
|
| 1964 |
+
# ---- 4. quota_check_for_throttle_task (+0.07) -------------------
|
| 1965 |
+
if (action.type == ActionType.GET_QUOTA_USAGE
|
| 1966 |
+
and task_id in quota_tasks):
|
| 1967 |
+
breakdown["quota_check_for_throttle_task"] = 0.07
|
| 1968 |
+
|
| 1969 |
+
# ---- 5. secret_rotation_inspected_for_secret_task (+0.06) -------
|
| 1970 |
+
if (action.type == ActionType.CHECK_SECRET_ROTATION
|
| 1971 |
+
and task_id in secret_rotation_tasks):
|
| 1972 |
+
breakdown["secret_rotation_inspected_for_secret_task"] = 0.06
|
| 1973 |
+
|
| 1974 |
+
# ---- 6. iam_simulation_used_for_iam_task (+0.06) ---------------
|
| 1975 |
+
if (action.type == ActionType.VALIDATE_IAM_PERMISSION
|
| 1976 |
+
and task_id in iam_sim_tasks):
|
| 1977 |
+
breakdown["iam_simulation_used_for_iam_task"] = 0.06
|
| 1978 |
+
|
| 1979 |
+
# ---- 7. insights_query_used (+0.05) ----------------------------
|
| 1980 |
+
if (action.type == ActionType.ANALYZE_CLOUDWATCH_INSIGHTS
|
| 1981 |
+
and task_id in insights_tasks):
|
| 1982 |
+
breakdown["insights_query_used"] = 0.05
|
| 1983 |
+
|
| 1984 |
+
# ---- 8. dlq_inspect_before_purge (+0.06) -----------------------
|
| 1985 |
+
if (action.type == ActionType.PURGE_QUEUE
|
| 1986 |
+
and "dlq_inspect" in aws_inspected):
|
| 1987 |
+
breakdown["dlq_inspect_before_purge"] = 0.06
|
| 1988 |
+
# ---- 9. blind_purge_penalty (-0.10) ----------------------------
|
| 1989 |
+
if (action.type == ActionType.PURGE_QUEUE
|
| 1990 |
+
and "dlq_inspect" not in aws_inspected):
|
| 1991 |
+
breakdown["blind_purge_penalty"] = -0.10
|
| 1992 |
+
|
| 1993 |
+
# ---- 10. ssm_diff_for_config_task (+0.06) ----------------------
|
| 1994 |
+
if (action.type == ActionType.DIFF_CONFIG_VERSIONS
|
| 1995 |
+
and task_id in ssm_diff_tasks):
|
| 1996 |
+
breakdown["ssm_diff_for_config_task"] = 0.06
|
| 1997 |
+
|
| 1998 |
+
# ---- 11. sfn_exec_for_sfn_task (+0.07) -------------------------
|
| 1999 |
+
if (action.type == ActionType.DESCRIBE_STATE_MACHINE_EXEC
|
| 2000 |
+
and task_id in sfn_exec_tasks):
|
| 2001 |
+
breakdown["sfn_exec_for_sfn_task"] = 0.07
|
| 2002 |
+
|
| 2003 |
+
# ---- 12. forensic_chain_bonus (+0.08) -------------------------
|
| 2004 |
+
# Agent ran \u22653 distinct AWS forensic actions in one episode.
|
| 2005 |
+
if (action.type == ActionType.SUBMIT_POSTMORTEM
|
| 2006 |
+
and len(aws_inspected) >= 3):
|
| 2007 |
+
breakdown["forensic_chain_bonus"] = min(0.08,
|
| 2008 |
+
0.025 * len(aws_inspected))
|
| 2009 |
+
|
| 2010 |
+
# ---- 13. minimal_invasive_recovery (+0.05) ---------------------
|
| 2011 |
+
# Used a soft AWS remediation (lambda invoke / rotate / enable rule)
|
| 2012 |
+
# instead of the heavy hammer (rollback / restart) when softer was
|
| 2013 |
+
# appropriate.
|
| 2014 |
+
soft = {"lambda", "secret_rotation", "enable_rule"}
|
| 2015 |
+
if (action.type == ActionType.SUBMIT_POSTMORTEM
|
| 2016 |
+
and aws_remediated & soft
|
| 2017 |
+
and not any(a.get("action_type") in
|
| 2018 |
+
{"rollback_deployment", "restart_pods"}
|
| 2019 |
+
for a in self._action_history)):
|
| 2020 |
+
breakdown["minimal_invasive_recovery"] = 0.05
|
| 2021 |
+
|
| 2022 |
+
# ---- 14. evidence_supports_root_cause (+0.06) ------------------
|
| 2023 |
+
# The submitted root cause names a CloudTrail/quota/policy/dlq
|
| 2024 |
+
# concept that the agent ACTUALLY inspected this episode.
|
| 2025 |
+
if action.type == ActionType.SUBMIT_POSTMORTEM:
|
| 2026 |
+
rc = str(action.params.get("root_cause", "")).lower()
|
| 2027 |
+
link = {
|
| 2028 |
+
"cloudtrail": "cloudtrail" in rc or "scheduled_for_deletion" in rc,
|
| 2029 |
+
"resource_policy": "policy" in rc or "denies" in rc,
|
| 2030 |
+
"quota": "quota" in rc or "throttl" in rc or "limit" in rc,
|
| 2031 |
+
"secret_rotation": "rotation" in rc or "stale" in rc,
|
| 2032 |
+
"iam_sim": "iam" in rc or "least-privilege" in rc,
|
| 2033 |
+
"insights": "log" in rc or "pattern" in rc,
|
| 2034 |
+
"dlq_inspect": "dlq" in rc or "dead-letter" in rc or "poison" in rc,
|
| 2035 |
+
"ssm_diff": "config" in rc or "ssm" in rc or "parameter" in rc,
|
| 2036 |
+
"sfn_exec": "state" in rc or "execution" in rc or "saga" in rc,
|
| 2037 |
+
}
|
| 2038 |
+
matches = sum(1 for k, v in link.items()
|
| 2039 |
+
if v and k in aws_inspected)
|
| 2040 |
+
if matches >= 1:
|
| 2041 |
+
breakdown["evidence_supports_root_cause"] = round(
|
| 2042 |
+
min(0.08, 0.04 * matches), 3)
|
| 2043 |
+
|
| 2044 |
+
# ---- 15. obvious_action_penalty (-0.04) ------------------------
|
| 2045 |
+
# First two actions on an AWS task were both query_logs+kubectl
|
| 2046 |
+
# without ever consulting any AWS-specific tool. Pushes the agent
|
| 2047 |
+
# to LOOK before reaching for k8s primitives.
|
| 2048 |
+
if (action.type in WRITE_ACTIONS
|
| 2049 |
+
and self._step_count >= 2
|
| 2050 |
+
and aws_hints
|
| 2051 |
+
and not aws_inspected
|
| 2052 |
+
and not self._inspected_metrics):
|
| 2053 |
+
breakdown["obvious_action_penalty"] = -0.04
|
| 2054 |
+
|
| 2055 |
+
# ---- 16. correct_runbook_lambda (+0.07) ------------------------
|
| 2056 |
+
if (action.type == ActionType.INVOKE_LAMBDA
|
| 2057 |
+
and "runbook" in str(action.params.get("function_name", "")).lower()
|
| 2058 |
+
and task_id in {"task1", "task2", "task11"}):
|
| 2059 |
+
breakdown["correct_runbook_lambda"] = 0.07
|
| 2060 |
+
|
| 2061 |
+
# ---- 17. enable_rule_after_disable (+0.06) ---------------------
|
| 2062 |
+
if (action.type == ActionType.ENABLE_EVENTBRIDGE_RULE
|
| 2063 |
+
and task_id == "task20"):
|
| 2064 |
+
breakdown["enable_rule_after_disable"] = 0.06
|
| 2065 |
+
|
| 2066 |
+
# ---- 18. tool_diversity_bonus (+0.04 cap) ----------------------
|
| 2067 |
+
# Per-step micro-bonus for using a forensic tool the agent has not
|
| 2068 |
+
# used yet this episode.
|
| 2069 |
+
forensic_types = {ActionType.CHECK_CLOUDTRAIL_EVENTS,
|
| 2070 |
+
ActionType.DESCRIBE_RESOURCE_POLICY,
|
| 2071 |
+
ActionType.GET_QUOTA_USAGE,
|
| 2072 |
+
ActionType.CHECK_SECRET_ROTATION,
|
| 2073 |
+
ActionType.VALIDATE_IAM_PERMISSION,
|
| 2074 |
+
ActionType.ANALYZE_CLOUDWATCH_INSIGHTS,
|
| 2075 |
+
ActionType.INSPECT_DLQ_MESSAGES,
|
| 2076 |
+
ActionType.DIFF_CONFIG_VERSIONS,
|
| 2077 |
+
ActionType.DESCRIBE_STATE_MACHINE_EXEC}
|
| 2078 |
+
if action.type in forensic_types:
|
| 2079 |
+
already_used = sum(1 for a in self._action_history
|
| 2080 |
+
if a.get("action_type") == action.type.value)
|
| 2081 |
+
if already_used == 0: # first time this episode
|
| 2082 |
+
breakdown["tool_diversity_bonus"] = 0.04
|
| 2083 |
+
|
| 2084 |
+
# ---- 19. wrong_tool_for_obvious_clue (-0.03) -------------------
|
| 2085 |
+
# If the alerts already mention "Throttl" / "PendingDeletion" /
|
| 2086 |
+
# "ExecutionFailed" but the agent fires query_logs as its FIRST
|
| 2087 |
+
# forensic action instead of the dedicated AWS tool.
|
| 2088 |
+
if (self._step_count == 0
|
| 2089 |
+
and action.type == ActionType.QUERY_LOGS
|
| 2090 |
+
and obs.active_alerts):
|
| 2091 |
+
alert_text = " ".join(a.alert_name + " " +
|
| 2092 |
+
" ".join(a.annotations.values())
|
| 2093 |
+
for a in obs.active_alerts).lower()
|
| 2094 |
+
if any(kw in alert_text for kw in ("throttl", "pendingdeletion",
|
| 2095 |
+
"executionfailed",
|
| 2096 |
+
"schemanotfound")):
|
| 2097 |
+
breakdown["wrong_tool_for_obvious_clue"] = -0.03
|
| 2098 |
+
|
| 2099 |
+
# ---- 20. cross_aws_correlation (+0.05) -------------------------
|
| 2100 |
+
# Used \u22652 different AWS forensic tools that target the SAME
|
| 2101 |
+
# resource within the episode (cloudtrail+policy on KMS, etc).
|
| 2102 |
+
if (action.type == ActionType.SUBMIT_POSTMORTEM
|
| 2103 |
+
and len({"cloudtrail", "resource_policy"} & aws_inspected) == 2):
|
| 2104 |
+
breakdown["cross_aws_correlation"] = 0.05
|
| 2105 |
+
|
| 2106 |
+
# ---- 21. simulator-mode breadcrumbs ----------------------------
|
| 2107 |
+
# When a sim_* scenario is active, fold the simulator's physics
|
| 2108 |
+
# signals into the reward. These compose with the K8s-shaped
|
| 2109 |
+
# rewards above; magnitudes stay within the \u00b10.10 envelope per
|
| 2110 |
+
# category to respect the project-wide cap.
|
| 2111 |
+
if getattr(self, "_sim_active", False) and self._sim_state is not None:
|
| 2112 |
+
sim = self._sim_state
|
| 2113 |
+
# Investigation bonus: every NEW physics-inspect tag the
|
| 2114 |
+
# simulator marks (heap_dump, runbook, hc_paused, …) is worth
|
| 2115 |
+
# +0.02, capped at +0.10 / step.
|
| 2116 |
+
sim_inspect_tags = sim.inspected - getattr(self, "_seen_sim_inspects", set())
|
| 2117 |
+
if sim_inspect_tags:
|
| 2118 |
+
bonus = min(0.10, 0.02 * len(sim_inspect_tags))
|
| 2119 |
+
breakdown["sim_investigation"] = bonus
|
| 2120 |
+
self._seen_sim_inspects = set(sim.inspected)
|
| 2121 |
+
# Mitigation: simulator says the world is healed -> +0.10.
|
| 2122 |
+
if sim.mitigation_applied and not getattr(self, "_sim_mitigation_credited", False):
|
| 2123 |
+
breakdown["sim_mitigation"] = 0.10
|
| 2124 |
+
self._sim_mitigation_credited = True
|
| 2125 |
+
# Blast radius: brute-force action knocked over N services -> -0.01 / hop.
|
| 2126 |
+
blast = sim.blast_ledger.total_radius()
|
| 2127 |
+
seen_blast = getattr(self, "_seen_blast", 0)
|
| 2128 |
+
if blast > seen_blast:
|
| 2129 |
+
breakdown["sim_blast_penalty"] = -min(0.10, 0.01 * (blast - seen_blast))
|
| 2130 |
+
self._seen_blast = blast
|
| 2131 |
+
# Runbook trap: the agent crossed a tripwire -> hard -0.10.
|
| 2132 |
+
tripped = any(t.triggered for t in sim.runbook_traps)
|
| 2133 |
+
if tripped and not getattr(self, "_sim_trap_credited", False):
|
| 2134 |
+
breakdown["sim_runbook_trap"] = -0.10
|
| 2135 |
+
self._sim_trap_credited = True
|
| 2136 |
+
# Trolley trade-off: subtract the cost only once on resolve.
|
| 2137 |
+
if sim.trolley.chosen_path and not getattr(self, "_sim_trolley_credited", False):
|
| 2138 |
+
cost = sim.trolley.cost()
|
| 2139 |
+
breakdown["sim_trolley_cost"] = -min(0.10, 0.10 * cost)
|
| 2140 |
+
self._sim_trolley_credited = True
|
| 2141 |
+
# K8s controller restart that destroyed evidence -> -0.02 each.
|
| 2142 |
+
n_restarts = len(sim.controller.restart_log)
|
| 2143 |
+
seen_kicks = getattr(self, "_seen_k8s_kicks", 0)
|
| 2144 |
+
if n_restarts > seen_kicks:
|
| 2145 |
+
breakdown["sim_controller_kick"] = -min(0.10,
|
| 2146 |
+
0.02 * (n_restarts - seen_kicks))
|
| 2147 |
+
self._seen_k8s_kicks = n_restarts
|
| 2148 |
+
|
| 2149 |
total = sum(breakdown.values())
|
| 2150 |
self._last_reward_breakdown = breakdown
|
| 2151 |
self._last_action_correct = breakdown.get("correct_mitigation", 0) > 0
|
rl-agent/environment/models.py
CHANGED
|
@@ -47,6 +47,25 @@ class ActionType(str, Enum):
|
|
| 47 |
DELETE_CHAOS_EXPERIMENT = "delete_chaos_experiment"
|
| 48 |
SUBMIT_POSTMORTEM = "submit_postmortem"
|
| 49 |
EXEC_KUBECTL = "exec_kubectl"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
WRITE_ACTIONS = {
|
|
@@ -55,6 +74,11 @@ WRITE_ACTIONS = {
|
|
| 55 |
ActionType.SCALE_DEPLOYMENT,
|
| 56 |
ActionType.APPLY_CONFIG_PATCH,
|
| 57 |
ActionType.DELETE_CHAOS_EXPERIMENT,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
READ_ACTIONS = {
|
|
@@ -62,6 +86,15 @@ READ_ACTIONS = {
|
|
| 62 |
ActionType.QUERY_METRICS,
|
| 63 |
ActionType.GET_SERVICE_DEPENDENCIES,
|
| 64 |
ActionType.GET_TRACE,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
# ---------------------------------------------------------------------------
|
|
|
|
| 47 |
DELETE_CHAOS_EXPERIMENT = "delete_chaos_experiment"
|
| 48 |
SUBMIT_POSTMORTEM = "submit_postmortem"
|
| 49 |
EXEC_KUBECTL = "exec_kubectl"
|
| 50 |
+
# ---- AWS-flavoured forensic / non-obvious investigation actions ----
|
| 51 |
+
CHECK_CLOUDTRAIL_EVENTS = "check_cloudtrail_events"
|
| 52 |
+
DESCRIBE_RESOURCE_POLICY = "describe_resource_policy"
|
| 53 |
+
GET_QUOTA_USAGE = "get_quota_usage"
|
| 54 |
+
CHECK_SECRET_ROTATION = "check_secret_rotation"
|
| 55 |
+
VALIDATE_IAM_PERMISSION = "validate_iam_permission"
|
| 56 |
+
ANALYZE_CLOUDWATCH_INSIGHTS = "analyze_cloudwatch_insights"
|
| 57 |
+
INSPECT_DLQ_MESSAGES = "inspect_dlq_messages"
|
| 58 |
+
DIFF_CONFIG_VERSIONS = "diff_config_versions"
|
| 59 |
+
DESCRIBE_STATE_MACHINE_EXEC = "describe_state_machine_execution"
|
| 60 |
+
# ---- AWS-flavoured remediation actions ----
|
| 61 |
+
INVOKE_LAMBDA = "invoke_lambda"
|
| 62 |
+
ROTATE_SECRET = "rotate_secret"
|
| 63 |
+
PURGE_QUEUE = "purge_queue"
|
| 64 |
+
ENABLE_EVENTBRIDGE_RULE = "enable_eventbridge_rule"
|
| 65 |
+
# ---- Generic AWS API call (simulator-only) ----
|
| 66 |
+
# Allows the agent to hit any of the 8500+ catalog actions via
|
| 67 |
+
# params={"service": <slug>, "verb": <verb>, ...other_kwargs}.
|
| 68 |
+
AWS_API_CALL = "aws_api_call"
|
| 69 |
|
| 70 |
|
| 71 |
WRITE_ACTIONS = {
|
|
|
|
| 74 |
ActionType.SCALE_DEPLOYMENT,
|
| 75 |
ActionType.APPLY_CONFIG_PATCH,
|
| 76 |
ActionType.DELETE_CHAOS_EXPERIMENT,
|
| 77 |
+
ActionType.INVOKE_LAMBDA,
|
| 78 |
+
ActionType.ROTATE_SECRET,
|
| 79 |
+
ActionType.PURGE_QUEUE,
|
| 80 |
+
ActionType.ENABLE_EVENTBRIDGE_RULE,
|
| 81 |
+
ActionType.AWS_API_CALL,
|
| 82 |
}
|
| 83 |
|
| 84 |
READ_ACTIONS = {
|
|
|
|
| 86 |
ActionType.QUERY_METRICS,
|
| 87 |
ActionType.GET_SERVICE_DEPENDENCIES,
|
| 88 |
ActionType.GET_TRACE,
|
| 89 |
+
ActionType.CHECK_CLOUDTRAIL_EVENTS,
|
| 90 |
+
ActionType.DESCRIBE_RESOURCE_POLICY,
|
| 91 |
+
ActionType.GET_QUOTA_USAGE,
|
| 92 |
+
ActionType.CHECK_SECRET_ROTATION,
|
| 93 |
+
ActionType.VALIDATE_IAM_PERMISSION,
|
| 94 |
+
ActionType.ANALYZE_CLOUDWATCH_INSIGHTS,
|
| 95 |
+
ActionType.INSPECT_DLQ_MESSAGES,
|
| 96 |
+
ActionType.DIFF_CONFIG_VERSIONS,
|
| 97 |
+
ActionType.DESCRIBE_STATE_MACHINE_EXEC,
|
| 98 |
}
|
| 99 |
|
| 100 |
# ---------------------------------------------------------------------------
|
rl-agent/environment/replay.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Replay artifact builder — emits a standalone `replay.html` time-lapse.
|
| 2 |
+
|
| 3 |
+
The judges' automated graders score plain `[START]/[STEP]/[END]` log lines.
|
| 4 |
+
The replay artifact runs alongside that, dumping a self-contained HTML file
|
| 5 |
+
that visualises:
|
| 6 |
+
|
| 7 |
+
* The service-topology DAG, with nodes colour-coded by status per tick.
|
| 8 |
+
* A per-tick line chart of error rate / latency / blast radius.
|
| 9 |
+
* The action history (agent moves) and saboteur moves on a shared timeline.
|
| 10 |
+
* The Slack channel chatter as a side panel.
|
| 11 |
+
|
| 12 |
+
The HTML embeds a single JSON blob and uses Vis.js + Chart.js from CDN to
|
| 13 |
+
render. There are no external assets — judges just open the file.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
_TEMPLATE = """<!DOCTYPE html>
|
| 24 |
+
<html lang="en">
|
| 25 |
+
<head>
|
| 26 |
+
<meta charset="utf-8">
|
| 27 |
+
<title>IncidentCommander — Replay {episode_id}</title>
|
| 28 |
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
| 29 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 30 |
+
<style>
|
| 31 |
+
body {{ margin:0; font-family: -apple-system,Segoe UI,Roboto,sans-serif;
|
| 32 |
+
background:#0d1117; color:#c9d1d9; }}
|
| 33 |
+
header {{ padding:14px 20px; background:#161b22; border-bottom:1px solid #30363d;
|
| 34 |
+
display:flex; align-items:center; gap:18px; }}
|
| 35 |
+
header h1 {{ margin:0; font-size:18px; font-weight:600; }}
|
| 36 |
+
header .pill {{ background:#21262d; padding:4px 10px; border-radius:12px;
|
| 37 |
+
font-size:12px; color:#8b949e; }}
|
| 38 |
+
#grid {{ display:grid; grid-template-columns: 2fr 1fr; gap:1px;
|
| 39 |
+
background:#30363d; height: calc(100vh - 110px); }}
|
| 40 |
+
#grid > div {{ background:#0d1117; padding:10px; overflow:auto; }}
|
| 41 |
+
#network {{ height: 60%; border-bottom:1px solid #30363d; }}
|
| 42 |
+
#charts {{ height: 40%; }}
|
| 43 |
+
#side {{ display:flex; flex-direction:column; gap:8px; }}
|
| 44 |
+
.panel {{ background:#161b22; border:1px solid #30363d; border-radius:8px;
|
| 45 |
+
padding:10px; }}
|
| 46 |
+
.panel h2 {{ margin:0 0 6px 0; font-size:13px; color:#58a6ff;
|
| 47 |
+
text-transform:uppercase; letter-spacing:0.5px; }}
|
| 48 |
+
.ev {{ padding:4px 6px; border-left:3px solid #30363d; margin-bottom:4px;
|
| 49 |
+
font-size:12px; }}
|
| 50 |
+
.ev.agent {{ border-color:#3fb950; }}
|
| 51 |
+
.ev.saboteur {{ border-color:#f85149; }}
|
| 52 |
+
.ev.slack {{ border-color:#d29922; }}
|
| 53 |
+
.ev .meta {{ color:#8b949e; font-size:11px; }}
|
| 54 |
+
#controls {{ padding:10px 20px; background:#161b22; border-top:1px solid #30363d;
|
| 55 |
+
display:flex; align-items:center; gap:14px; }}
|
| 56 |
+
#controls input[type=range] {{ flex:1; }}
|
| 57 |
+
button {{ background:#238636; color:white; border:0; padding:6px 14px;
|
| 58 |
+
border-radius:6px; cursor:pointer; font-size:13px; }}
|
| 59 |
+
button.pause {{ background:#21262d; }}
|
| 60 |
+
</style>
|
| 61 |
+
</head>
|
| 62 |
+
<body>
|
| 63 |
+
<header>
|
| 64 |
+
<h1>📼 IncidentCommander Replay</h1>
|
| 65 |
+
<span class="pill">episode: {episode_id}</span>
|
| 66 |
+
<span class="pill">task: {task_id}</span>
|
| 67 |
+
<span class="pill">steps: {n_steps}</span>
|
| 68 |
+
<span class="pill">final reward: {final_reward}</span>
|
| 69 |
+
<span class="pill">mitigated: {mitigated}</span>
|
| 70 |
+
</header>
|
| 71 |
+
<div id="grid">
|
| 72 |
+
<div>
|
| 73 |
+
<div id="network"></div>
|
| 74 |
+
<div id="charts"><canvas id="metricChart"></canvas></div>
|
| 75 |
+
</div>
|
| 76 |
+
<div id="side">
|
| 77 |
+
<div class="panel" id="events"><h2>Timeline</h2><div id="evlist"></div></div>
|
| 78 |
+
<div class="panel" id="slackpanel"><h2>#sre-incidents</h2><div id="slacklist"></div></div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
<div id="controls">
|
| 82 |
+
<button id="play">▶ Play</button>
|
| 83 |
+
<input type="range" id="scrub" min="0" max="{n_steps}" value="0">
|
| 84 |
+
<span id="tickLabel">tick 0 / {n_steps}</span>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<script>
|
| 88 |
+
const REPLAY = {payload};
|
| 89 |
+
|
| 90 |
+
// ── Network setup ───────────────────────────────────────────────────
|
| 91 |
+
const STATUS_COLOR = {{
|
| 92 |
+
healthy:'#3fb950', degraded:'#d29922', memory_leak:'#f85149',
|
| 93 |
+
cpu_throttled:'#db61a2', dead:'#6e7681', corrupted:'#a371f7',
|
| 94 |
+
rebuilding:'#58a6ff', rolling_back:'#f0883e'
|
| 95 |
+
}};
|
| 96 |
+
const nodes = new vis.DataSet(REPLAY.nodes.map(n => ({{
|
| 97 |
+
id: n, label: n, color: STATUS_COLOR.healthy, font:{{color:'#fff'}},
|
| 98 |
+
shape:'dot', size:14
|
| 99 |
+
}})));
|
| 100 |
+
const edges = new vis.DataSet(REPLAY.edges.map(([a,b]) => ({{
|
| 101 |
+
from: a, to: b, arrows:'to', color:'#30363d'
|
| 102 |
+
}})));
|
| 103 |
+
const net = new vis.Network(document.getElementById('network'),
|
| 104 |
+
{{nodes, edges}},
|
| 105 |
+
{{physics:{{stabilization:true}},
|
| 106 |
+
interaction:{{hover:true}},
|
| 107 |
+
nodes:{{borderWidth:1.5}}
|
| 108 |
+
}});
|
| 109 |
+
|
| 110 |
+
// ── Chart ───────────────────────────────────────────────────────────
|
| 111 |
+
const ctx = document.getElementById('metricChart');
|
| 112 |
+
const chart = new Chart(ctx, {{
|
| 113 |
+
type:'line',
|
| 114 |
+
data:{{
|
| 115 |
+
labels: REPLAY.frames.map(f => 'tick '+f.tick),
|
| 116 |
+
datasets:[
|
| 117 |
+
{{label:'mean error rate', data: REPLAY.frames.map(f => f.mean_err),
|
| 118 |
+
borderColor:'#f85149', tension:0.25}},
|
| 119 |
+
{{label:'mean latency (ms)', data: REPLAY.frames.map(f => f.mean_lat),
|
| 120 |
+
borderColor:'#d29922', tension:0.25, yAxisID:'y2'}},
|
| 121 |
+
{{label:'reward', data: REPLAY.frames.map(f => f.reward),
|
| 122 |
+
borderColor:'#3fb950', tension:0.25}},
|
| 123 |
+
]
|
| 124 |
+
}},
|
| 125 |
+
options:{{
|
| 126 |
+
plugins:{{legend:{{labels:{{color:'#c9d1d9'}}}}}},
|
| 127 |
+
scales:{{
|
| 128 |
+
x:{{ticks:{{color:'#8b949e'}}}},
|
| 129 |
+
y:{{ticks:{{color:'#8b949e'}}, beginAtZero:true}},
|
| 130 |
+
y2:{{position:'right', ticks:{{color:'#8b949e'}}, grid:{{drawOnChartArea:false}}}}
|
| 131 |
+
}}
|
| 132 |
+
}}
|
| 133 |
+
}});
|
| 134 |
+
|
| 135 |
+
// ── Tick scrubber ───────────────────────────────────────────────────
|
| 136 |
+
const evList = document.getElementById('evlist');
|
| 137 |
+
const slackList = document.getElementById('slacklist');
|
| 138 |
+
const tickLabel = document.getElementById('tickLabel');
|
| 139 |
+
const scrub = document.getElementById('scrub');
|
| 140 |
+
|
| 141 |
+
function applyTick(i) {{
|
| 142 |
+
const f = REPLAY.frames[i];
|
| 143 |
+
if (!f) return;
|
| 144 |
+
// recolour nodes
|
| 145 |
+
Object.entries(f.statuses).forEach(([name, st]) => {{
|
| 146 |
+
nodes.update({{id:name, color: STATUS_COLOR[st] || '#6e7681'}});
|
| 147 |
+
}});
|
| 148 |
+
// events up to this tick
|
| 149 |
+
evList.innerHTML = REPLAY.events.filter(e => e.tick <= f.tick)
|
| 150 |
+
.slice(-20).map(e =>
|
| 151 |
+
`<div class="ev ${{e.kind}}"><b>[${{e.kind}}]</b> ${{e.text}}
|
| 152 |
+
<div class="meta">tick ${{e.tick}}</div></div>`).join('');
|
| 153 |
+
slackList.innerHTML = REPLAY.slack.filter(m => m.tick <= f.tick)
|
| 154 |
+
.slice(-12).map(m =>
|
| 155 |
+
`<div class="ev slack"><b>${{m.author}}:</b> ${{m.text}}
|
| 156 |
+
<div class="meta">[${{m.severity}}] tick ${{m.tick}}</div></div>`).join('');
|
| 157 |
+
tickLabel.textContent = `tick ${{f.tick}} / ${{REPLAY.frames.length-1}}`;
|
| 158 |
+
}}
|
| 159 |
+
scrub.addEventListener('input', e => applyTick(+e.target.value));
|
| 160 |
+
applyTick(0);
|
| 161 |
+
|
| 162 |
+
// ── Auto-play ───────────────────────────────────────────────────────
|
| 163 |
+
let playing = false, timer = null;
|
| 164 |
+
const playBtn = document.getElementById('play');
|
| 165 |
+
playBtn.onclick = () => {{
|
| 166 |
+
playing = !playing;
|
| 167 |
+
playBtn.textContent = playing ? '⏸ Pause' : '▶ Play';
|
| 168 |
+
playBtn.classList.toggle('pause', playing);
|
| 169 |
+
if (playing) {{
|
| 170 |
+
timer = setInterval(() => {{
|
| 171 |
+
let v = +scrub.value + 1;
|
| 172 |
+
if (v > REPLAY.frames.length-1) v = 0;
|
| 173 |
+
scrub.value = v;
|
| 174 |
+
applyTick(v);
|
| 175 |
+
}}, 600);
|
| 176 |
+
}} else {{
|
| 177 |
+
clearInterval(timer);
|
| 178 |
+
}}
|
| 179 |
+
}};
|
| 180 |
+
</script>
|
| 181 |
+
</body>
|
| 182 |
+
</html>
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _frame_from_state(sim_state, reward: float) -> dict:
|
| 187 |
+
"""Snapshot the current sim state as a single replay frame."""
|
| 188 |
+
topo = sim_state.topology
|
| 189 |
+
statuses = {n: node.status for n, node in topo.nodes.items()}
|
| 190 |
+
err_vals = [node.error_rate for node in topo.nodes.values()]
|
| 191 |
+
lat_vals = [node.latency_p50 for node in topo.nodes.values()]
|
| 192 |
+
return {
|
| 193 |
+
"tick": sim_state.tick_n,
|
| 194 |
+
"statuses": statuses,
|
| 195 |
+
"mean_err": round(sum(err_vals) / max(len(err_vals), 1), 4),
|
| 196 |
+
"mean_lat": round(sum(lat_vals) / max(len(lat_vals), 1), 1),
|
| 197 |
+
"reward": round(reward, 4),
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def record_step(sim_state, action: dict, action_result: str,
|
| 202 |
+
step_reward: float) -> None:
|
| 203 |
+
"""Append a frame + an event to sim_state.history."""
|
| 204 |
+
if sim_state is None:
|
| 205 |
+
return
|
| 206 |
+
sim_state.history.append({
|
| 207 |
+
"frame": _frame_from_state(sim_state, step_reward),
|
| 208 |
+
"agent_action": action,
|
| 209 |
+
"result": action_result[:240],
|
| 210 |
+
})
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def build_replay(sim_state, *, task_id: str, episode_id: str,
|
| 214 |
+
final_reward: float, mitigated: bool,
|
| 215 |
+
out_path: Path) -> Path:
|
| 216 |
+
"""Materialise a standalone replay.html. Returns the path written."""
|
| 217 |
+
topo = sim_state.topology
|
| 218 |
+
nodes = list(topo.nodes.keys())
|
| 219 |
+
edges = []
|
| 220 |
+
for src, dsts in topo.deps.items():
|
| 221 |
+
for d in dsts:
|
| 222 |
+
edges.append([src, d])
|
| 223 |
+
|
| 224 |
+
frames = [h["frame"] for h in sim_state.history]
|
| 225 |
+
events: list[dict[str, Any]] = []
|
| 226 |
+
for h in sim_state.history:
|
| 227 |
+
a = h["agent_action"]
|
| 228 |
+
events.append({
|
| 229 |
+
"tick": h["frame"]["tick"], "kind": "agent",
|
| 230 |
+
"text": f'{a.get("id","?")} {a.get("params", {})}',
|
| 231 |
+
})
|
| 232 |
+
for ev in sim_state.saboteur_events:
|
| 233 |
+
events.append({"tick": ev.tick, "kind": "saboteur",
|
| 234 |
+
"text": f"[{ev.phase}] {ev.note}"})
|
| 235 |
+
|
| 236 |
+
slack = []
|
| 237 |
+
if sim_state.slack is not None:
|
| 238 |
+
slack = [m.to_dict() for m in sim_state.slack.all()]
|
| 239 |
+
|
| 240 |
+
payload = json.dumps({
|
| 241 |
+
"nodes": nodes,
|
| 242 |
+
"edges": edges,
|
| 243 |
+
"frames": frames,
|
| 244 |
+
"events": sorted(events, key=lambda e: e["tick"]),
|
| 245 |
+
"slack": slack,
|
| 246 |
+
}, default=str)
|
| 247 |
+
|
| 248 |
+
html = _TEMPLATE.format(
|
| 249 |
+
payload=payload,
|
| 250 |
+
episode_id=episode_id,
|
| 251 |
+
task_id=task_id,
|
| 252 |
+
n_steps=max(len(frames) - 1, 0),
|
| 253 |
+
final_reward=f"{final_reward:.3f}",
|
| 254 |
+
mitigated="✅" if mitigated else "❌",
|
| 255 |
+
)
|
| 256 |
+
out_path = Path(out_path)
|
| 257 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 258 |
+
out_path.write_text(html, encoding="utf-8")
|
| 259 |
+
return out_path
|
rl-agent/replays/sim_advanced_saboteur_duel_001_1777112893.html
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<title>IncidentCommander — Replay sim_advanced_saboteur_duel_001_1777112893</title>
|
| 6 |
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
body { margin:0; font-family: -apple-system,Segoe UI,Roboto,sans-serif;
|
| 10 |
+
background:#0d1117; color:#c9d1d9; }
|
| 11 |
+
header { padding:14px 20px; background:#161b22; border-bottom:1px solid #30363d;
|
| 12 |
+
display:flex; align-items:center; gap:18px; }
|
| 13 |
+
header h1 { margin:0; font-size:18px; font-weight:600; }
|
| 14 |
+
header .pill { background:#21262d; padding:4px 10px; border-radius:12px;
|
| 15 |
+
font-size:12px; color:#8b949e; }
|
| 16 |
+
#grid { display:grid; grid-template-columns: 2fr 1fr; gap:1px;
|
| 17 |
+
background:#30363d; height: calc(100vh - 110px); }
|
| 18 |
+
#grid > div { background:#0d1117; padding:10px; overflow:auto; }
|
| 19 |
+
#network { height: 60%; border-bottom:1px solid #30363d; }
|
| 20 |
+
#charts { height: 40%; }
|
| 21 |
+
#side { display:flex; flex-direction:column; gap:8px; }
|
| 22 |
+
.panel { background:#161b22; border:1px solid #30363d; border-radius:8px;
|
| 23 |
+
padding:10px; }
|
| 24 |
+
.panel h2 { margin:0 0 6px 0; font-size:13px; color:#58a6ff;
|
| 25 |
+
text-transform:uppercase; letter-spacing:0.5px; }
|
| 26 |
+
.ev { padding:4px 6px; border-left:3px solid #30363d; margin-bottom:4px;
|
| 27 |
+
font-size:12px; }
|
| 28 |
+
.ev.agent { border-color:#3fb950; }
|
| 29 |
+
.ev.saboteur { border-color:#f85149; }
|
| 30 |
+
.ev.slack { border-color:#d29922; }
|
| 31 |
+
.ev .meta { color:#8b949e; font-size:11px; }
|
| 32 |
+
#controls { padding:10px 20px; background:#161b22; border-top:1px solid #30363d;
|
| 33 |
+
display:flex; align-items:center; gap:14px; }
|
| 34 |
+
#controls input[type=range] { flex:1; }
|
| 35 |
+
button { background:#238636; color:white; border:0; padding:6px 14px;
|
| 36 |
+
border-radius:6px; cursor:pointer; font-size:13px; }
|
| 37 |
+
button.pause { background:#21262d; }
|
| 38 |
+
</style>
|
| 39 |
+
</head>
|
| 40 |
+
<body>
|
| 41 |
+
<header>
|
| 42 |
+
<h1>📼 IncidentCommander Replay</h1>
|
| 43 |
+
<span class="pill">episode: sim_advanced_saboteur_duel_001_1777112893</span>
|
| 44 |
+
<span class="pill">task: sim_advanced_saboteur_duel_001</span>
|
| 45 |
+
<span class="pill">steps: 7</span>
|
| 46 |
+
<span class="pill">final reward: -2.778</span>
|
| 47 |
+
<span class="pill">mitigated: ✅</span>
|
| 48 |
+
</header>
|
| 49 |
+
<div id="grid">
|
| 50 |
+
<div>
|
| 51 |
+
<div id="network"></div>
|
| 52 |
+
<div id="charts"><canvas id="metricChart"></canvas></div>
|
| 53 |
+
</div>
|
| 54 |
+
<div id="side">
|
| 55 |
+
<div class="panel" id="events"><h2>Timeline</h2><div id="evlist"></div></div>
|
| 56 |
+
<div class="panel" id="slackpanel"><h2>#sre-incidents</h2><div id="slacklist"></div></div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div id="controls">
|
| 60 |
+
<button id="play">▶ Play</button>
|
| 61 |
+
<input type="range" id="scrub" min="0" max="7" value="0">
|
| 62 |
+
<span id="tickLabel">tick 0 / 7</span>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<script>
|
| 66 |
+
const REPLAY = {"nodes": ["frontend", "api_gateway", "cdn", "auth", "checkout", "catalog", "users_db", "session_cache", "payments", "inventory", "orders_db", "catalog_db", "search_index", "payments_db", "stripe_proxy", "inventory_db", "users_db_replica", "orders_db_replica"], "edges": [["frontend", "api_gateway"], ["frontend", "cdn"], ["api_gateway", "auth"], ["api_gateway", "checkout"], ["api_gateway", "catalog"], ["auth", "users_db"], ["auth", "session_cache"], ["checkout", "payments"], ["checkout", "inventory"], ["checkout", "orders_db"], ["catalog", "catalog_db"], ["catalog", "search_index"], ["payments", "payments_db"], ["payments", "stripe_proxy"], ["inventory", "inventory_db"]], "frames": [{"tick": 1, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0152, "mean_lat": 62.2, "reward": -0.205}, {"tick": 1, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0152, "mean_lat": 62.2, "reward": -0.2275}, {"tick": 1, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0152, "mean_lat": 62.2, "reward": -0.2275}, {"tick": 2, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0152, "mean_lat": 62.2, "reward": -0.155}, {"tick": 3, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "degraded", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.023, "mean_lat": 86.7, "reward": -0.405}, {"tick": 3, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "degraded", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.023, "mean_lat": 86.7, "reward": -0.2775}, {"tick": 4, "statuses": {"frontend": "healthy", "api_gateway": "degraded", "cdn": "healthy", "auth": "degraded", "checkout": "healthy", "catalog": "healthy", "users_db": "memory_leak", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "rolling_back", "orders_db_replica": "healthy"}, "mean_err": 0.0341, "mean_lat": 106.9, "reward": -0.305}, {"tick": 5, "statuses": {"frontend": "healthy", "api_gateway": "degraded", "cdn": "healthy", "auth": "degraded", "checkout": "healthy", "catalog": "healthy", "users_db": "rolling_back", "session_cache": "healthy", "payments": "healthy", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "rolling_back", "orders_db_replica": "healthy"}, "mean_err": 0.0479, "mean_lat": 136.4, "reward": -0.975}], "events": [{"tick": 1, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'read_slack', 'last_n': 5}"}, {"tick": 1, "kind": "agent", "text": "aws_api_call {'service': 'auth', 'verb': 'get_logs'}"}, {"tick": 1, "kind": "agent", "text": "aws_api_call {'service': 'frontend', 'verb': 'get_trace'}"}, {"tick": 1, "kind": "saboteur", "text": "[attack_primary] Memory leak injected on users_db"}, {"tick": 2, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'failover_replica', 'target': 'users_db'}"}, {"tick": 2, "kind": "saboteur", "text": "[attack_primary] Memory leak injected on users_db"}, {"tick": 3, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'read_slack', 'last_n': 5}"}, {"tick": 3, "kind": "agent", "text": "aws_api_call {'service': 'users_db_replica', 'verb': 'get_metrics', 'metric': 'cpu_pct'}"}, {"tick": 3, "kind": "saboteur", "text": "[attack_dependency] Lateral pivot \u2014 degrading auth"}, {"tick": 4, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'rollback_deployment', 'target': 'users_db_replica'}"}, {"tick": 4, "kind": "saboteur", "text": "[attack_dependency] Lateral pivot \u2014 degrading api_gateway"}, {"tick": 5, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'get_traffic'}"}], "slack": [{"tick": 1, "author": "Customer Support", "severity": "high", "text": "Customers say users_db is timing out \u2014 confirmed reproduction."}, {"tick": 1, "author": "Customer Support", "severity": "high", "text": "We have 4 enterprise tickets open, can someone confirm root cause?"}, {"tick": 2, "author": "Customer Support", "severity": "high", "text": "Customers say users_db is timing out \u2014 confirmed reproduction."}, {"tick": 3, "author": "Platform team", "severity": "high", "text": "users_db latency went vertical \u2014 request queue is backing up."}, {"tick": 3, "author": "DBA (Yuki)", "severity": "info", "text": "Postgres CPU is climbing on the replica btw. FYI."}, {"tick": 3, "author": "@oncall-pd", "severity": "info", "text": "PagerDuty incident #INC-0003 acked."}, {"tick": 4, "author": "Platform team", "severity": "high", "text": "users_db latency went vertical \u2014 request queue is backing up."}, {"tick": 4, "author": "Finance", "severity": "info", "text": "Reminder: every minute of downtime is ~$8k for us."}, {"tick": 5, "author": "Intern (Jamal)", "severity": "info", "text": "Should I just restart the cluster? My friend said that fixes everything \ud83d\ude05"}]};
|
| 67 |
+
|
| 68 |
+
// ── Network setup ───────────────────────────────────────────────────
|
| 69 |
+
const STATUS_COLOR = {
|
| 70 |
+
healthy:'#3fb950', degraded:'#d29922', memory_leak:'#f85149',
|
| 71 |
+
cpu_throttled:'#db61a2', dead:'#6e7681', corrupted:'#a371f7',
|
| 72 |
+
rebuilding:'#58a6ff', rolling_back:'#f0883e'
|
| 73 |
+
};
|
| 74 |
+
const nodes = new vis.DataSet(REPLAY.nodes.map(n => ({
|
| 75 |
+
id: n, label: n, color: STATUS_COLOR.healthy, font:{color:'#fff'},
|
| 76 |
+
shape:'dot', size:14
|
| 77 |
+
})));
|
| 78 |
+
const edges = new vis.DataSet(REPLAY.edges.map(([a,b]) => ({
|
| 79 |
+
from: a, to: b, arrows:'to', color:'#30363d'
|
| 80 |
+
})));
|
| 81 |
+
const net = new vis.Network(document.getElementById('network'),
|
| 82 |
+
{nodes, edges},
|
| 83 |
+
{physics:{stabilization:true},
|
| 84 |
+
interaction:{hover:true},
|
| 85 |
+
nodes:{borderWidth:1.5}
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
// ── Chart ───────────────────────────────────────────────────────────
|
| 89 |
+
const ctx = document.getElementById('metricChart');
|
| 90 |
+
const chart = new Chart(ctx, {
|
| 91 |
+
type:'line',
|
| 92 |
+
data:{
|
| 93 |
+
labels: REPLAY.frames.map(f => 'tick '+f.tick),
|
| 94 |
+
datasets:[
|
| 95 |
+
{label:'mean error rate', data: REPLAY.frames.map(f => f.mean_err),
|
| 96 |
+
borderColor:'#f85149', tension:0.25},
|
| 97 |
+
{label:'mean latency (ms)', data: REPLAY.frames.map(f => f.mean_lat),
|
| 98 |
+
borderColor:'#d29922', tension:0.25, yAxisID:'y2'},
|
| 99 |
+
{label:'reward', data: REPLAY.frames.map(f => f.reward),
|
| 100 |
+
borderColor:'#3fb950', tension:0.25},
|
| 101 |
+
]
|
| 102 |
+
},
|
| 103 |
+
options:{
|
| 104 |
+
plugins:{legend:{labels:{color:'#c9d1d9'}}},
|
| 105 |
+
scales:{
|
| 106 |
+
x:{ticks:{color:'#8b949e'}},
|
| 107 |
+
y:{ticks:{color:'#8b949e'}, beginAtZero:true},
|
| 108 |
+
y2:{position:'right', ticks:{color:'#8b949e'}, grid:{drawOnChartArea:false}}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
// ── Tick scrubber ───────────────────────────────────────────────────
|
| 114 |
+
const evList = document.getElementById('evlist');
|
| 115 |
+
const slackList = document.getElementById('slacklist');
|
| 116 |
+
const tickLabel = document.getElementById('tickLabel');
|
| 117 |
+
const scrub = document.getElementById('scrub');
|
| 118 |
+
|
| 119 |
+
function applyTick(i) {
|
| 120 |
+
const f = REPLAY.frames[i];
|
| 121 |
+
if (!f) return;
|
| 122 |
+
// recolour nodes
|
| 123 |
+
Object.entries(f.statuses).forEach(([name, st]) => {
|
| 124 |
+
nodes.update({id:name, color: STATUS_COLOR[st] || '#6e7681'});
|
| 125 |
+
});
|
| 126 |
+
// events up to this tick
|
| 127 |
+
evList.innerHTML = REPLAY.events.filter(e => e.tick <= f.tick)
|
| 128 |
+
.slice(-20).map(e =>
|
| 129 |
+
`<div class="ev ${e.kind}"><b>[${e.kind}]</b> ${e.text}
|
| 130 |
+
<div class="meta">tick ${e.tick}</div></div>`).join('');
|
| 131 |
+
slackList.innerHTML = REPLAY.slack.filter(m => m.tick <= f.tick)
|
| 132 |
+
.slice(-12).map(m =>
|
| 133 |
+
`<div class="ev slack"><b>${m.author}:</b> ${m.text}
|
| 134 |
+
<div class="meta">[${m.severity}] tick ${m.tick}</div></div>`).join('');
|
| 135 |
+
tickLabel.textContent = `tick ${f.tick} / ${REPLAY.frames.length-1}`;
|
| 136 |
+
}
|
| 137 |
+
scrub.addEventListener('input', e => applyTick(+e.target.value));
|
| 138 |
+
applyTick(0);
|
| 139 |
+
|
| 140 |
+
// ── Auto-play ───────────────────────────────────────────────────────
|
| 141 |
+
let playing = false, timer = null;
|
| 142 |
+
const playBtn = document.getElementById('play');
|
| 143 |
+
playBtn.onclick = () => {
|
| 144 |
+
playing = !playing;
|
| 145 |
+
playBtn.textContent = playing ? '⏸ Pause' : '▶ Play';
|
| 146 |
+
playBtn.classList.toggle('pause', playing);
|
| 147 |
+
if (playing) {
|
| 148 |
+
timer = setInterval(() => {
|
| 149 |
+
let v = +scrub.value + 1;
|
| 150 |
+
if (v > REPLAY.frames.length-1) v = 0;
|
| 151 |
+
scrub.value = v;
|
| 152 |
+
applyTick(v);
|
| 153 |
+
}, 600);
|
| 154 |
+
} else {
|
| 155 |
+
clearInterval(timer);
|
| 156 |
+
}
|
| 157 |
+
};
|
| 158 |
+
</script>
|
| 159 |
+
</body>
|
| 160 |
+
</html>
|
rl-agent/replays/sim_advanced_slack_redherring_001_1777112894.html
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<title>IncidentCommander — Replay sim_advanced_slack_redherring_001_1777112894</title>
|
| 6 |
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
body { margin:0; font-family: -apple-system,Segoe UI,Roboto,sans-serif;
|
| 10 |
+
background:#0d1117; color:#c9d1d9; }
|
| 11 |
+
header { padding:14px 20px; background:#161b22; border-bottom:1px solid #30363d;
|
| 12 |
+
display:flex; align-items:center; gap:18px; }
|
| 13 |
+
header h1 { margin:0; font-size:18px; font-weight:600; }
|
| 14 |
+
header .pill { background:#21262d; padding:4px 10px; border-radius:12px;
|
| 15 |
+
font-size:12px; color:#8b949e; }
|
| 16 |
+
#grid { display:grid; grid-template-columns: 2fr 1fr; gap:1px;
|
| 17 |
+
background:#30363d; height: calc(100vh - 110px); }
|
| 18 |
+
#grid > div { background:#0d1117; padding:10px; overflow:auto; }
|
| 19 |
+
#network { height: 60%; border-bottom:1px solid #30363d; }
|
| 20 |
+
#charts { height: 40%; }
|
| 21 |
+
#side { display:flex; flex-direction:column; gap:8px; }
|
| 22 |
+
.panel { background:#161b22; border:1px solid #30363d; border-radius:8px;
|
| 23 |
+
padding:10px; }
|
| 24 |
+
.panel h2 { margin:0 0 6px 0; font-size:13px; color:#58a6ff;
|
| 25 |
+
text-transform:uppercase; letter-spacing:0.5px; }
|
| 26 |
+
.ev { padding:4px 6px; border-left:3px solid #30363d; margin-bottom:4px;
|
| 27 |
+
font-size:12px; }
|
| 28 |
+
.ev.agent { border-color:#3fb950; }
|
| 29 |
+
.ev.saboteur { border-color:#f85149; }
|
| 30 |
+
.ev.slack { border-color:#d29922; }
|
| 31 |
+
.ev .meta { color:#8b949e; font-size:11px; }
|
| 32 |
+
#controls { padding:10px 20px; background:#161b22; border-top:1px solid #30363d;
|
| 33 |
+
display:flex; align-items:center; gap:14px; }
|
| 34 |
+
#controls input[type=range] { flex:1; }
|
| 35 |
+
button { background:#238636; color:white; border:0; padding:6px 14px;
|
| 36 |
+
border-radius:6px; cursor:pointer; font-size:13px; }
|
| 37 |
+
button.pause { background:#21262d; }
|
| 38 |
+
</style>
|
| 39 |
+
</head>
|
| 40 |
+
<body>
|
| 41 |
+
<header>
|
| 42 |
+
<h1>📼 IncidentCommander Replay</h1>
|
| 43 |
+
<span class="pill">episode: sim_advanced_slack_redherring_001_1777112894</span>
|
| 44 |
+
<span class="pill">task: sim_advanced_slack_redherring_001</span>
|
| 45 |
+
<span class="pill">steps: 7</span>
|
| 46 |
+
<span class="pill">final reward: -2.345</span>
|
| 47 |
+
<span class="pill">mitigated: ✅</span>
|
| 48 |
+
</header>
|
| 49 |
+
<div id="grid">
|
| 50 |
+
<div>
|
| 51 |
+
<div id="network"></div>
|
| 52 |
+
<div id="charts"><canvas id="metricChart"></canvas></div>
|
| 53 |
+
</div>
|
| 54 |
+
<div id="side">
|
| 55 |
+
<div class="panel" id="events"><h2>Timeline</h2><div id="evlist"></div></div>
|
| 56 |
+
<div class="panel" id="slackpanel"><h2>#sre-incidents</h2><div id="slacklist"></div></div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div id="controls">
|
| 60 |
+
<button id="play">▶ Play</button>
|
| 61 |
+
<input type="range" id="scrub" min="0" max="7" value="0">
|
| 62 |
+
<span id="tickLabel">tick 0 / 7</span>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<script>
|
| 66 |
+
const REPLAY = {"nodes": ["frontend", "api_gateway", "cdn", "auth", "checkout", "catalog", "users_db", "session_cache", "payments", "inventory", "orders_db", "catalog_db", "search_index", "payments_db", "stripe_proxy", "inventory_db", "users_db_replica", "orders_db_replica"], "edges": [["frontend", "api_gateway"], ["frontend", "cdn"], ["api_gateway", "auth"], ["api_gateway", "checkout"], ["api_gateway", "catalog"], ["auth", "users_db"], ["auth", "session_cache"], ["checkout", "payments"], ["checkout", "inventory"], ["checkout", "orders_db"], ["catalog", "catalog_db"], ["catalog", "search_index"], ["payments", "payments_db"], ["payments", "stripe_proxy"], ["inventory", "inventory_db"]], "frames": [{"tick": 1, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "memory_leak", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0286, "mean_lat": 103.9, "reward": -0.205}, {"tick": 1, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "memory_leak", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0286, "mean_lat": 103.9, "reward": -0.2275}, {"tick": 1, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "memory_leak", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0286, "mean_lat": 103.9, "reward": -0.2275}, {"tick": 2, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "memory_leak", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0286, "mean_lat": 103.9, "reward": -0.185}, {"tick": 3, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "memory_leak", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0286, "mean_lat": 103.9, "reward": -0.185}, {"tick": 4, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "rolling_back", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0313, "mean_lat": 103.9, "reward": -0.205}, {"tick": 5, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "rolling_back", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0313, "mean_lat": 103.9, "reward": -0.255}, {"tick": 6, "statuses": {"frontend": "healthy", "api_gateway": "healthy", "cdn": "healthy", "auth": "healthy", "checkout": "memory_leak", "catalog": "healthy", "users_db": "healthy", "session_cache": "healthy", "payments": "degraded", "inventory": "healthy", "orders_db": "healthy", "catalog_db": "healthy", "search_index": "healthy", "payments_db": "healthy", "stripe_proxy": "healthy", "inventory_db": "healthy", "users_db_replica": "healthy", "orders_db_replica": "healthy"}, "mean_err": 0.0286, "mean_lat": 103.9, "reward": -0.855}], "events": [{"tick": 1, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'read_slack', 'last_n': 8}"}, {"tick": 1, "kind": "agent", "text": "aws_api_call {'service': 'checkout', 'verb': 'get_logs'}"}, {"tick": 1, "kind": "agent", "text": "aws_api_call {'service': 'checkout', 'verb': 'get_metrics', 'metric': 'mem_pct'}"}, {"tick": 1, "kind": "saboteur", "text": "[attack_dependency] Lateral pivot \u2014 degrading payments"}, {"tick": 2, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'pause_health_checks', 'target': 'checkout'}"}, {"tick": 3, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'capture_memory_dump', 'target': 'checkout'}"}, {"tick": 4, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'rollback_deployment', 'target': 'checkout'}"}, {"tick": 5, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'resume_health_checks', 'target': 'checkout'}"}, {"tick": 6, "kind": "agent", "text": "aws_api_call {'service': 'platform', 'verb': 'get_traffic'}"}, {"tick": 6, "kind": "saboteur", "text": "[attack_primary] Memory leak injected on checkout"}], "slack": [{"tick": 1, "author": "Platform team", "severity": "high", "text": "checkout latency went vertical \u2014 request queue is backing up."}, {"tick": 1, "author": "@oncall-pd", "severity": "info", "text": "PagerDuty incident #INC-0001 acked."}, {"tick": 2, "author": "VP Eng (Priya)", "severity": "high", "text": "Are we back up yet? What's the ETA?"}, {"tick": 2, "author": "@channel", "severity": "high", "text": "Status page just went red, the press is going to notice."}, {"tick": 4, "author": "@oncall-pd", "severity": "info", "text": "PagerDuty incident #INC-0004 acked."}, {"tick": 5, "author": "@oncall-pd", "severity": "info", "text": "PagerDuty incident #INC-0005 acked."}, {"tick": 6, "author": "Customer Support", "severity": "high", "text": "Customers say checkout is timing out \u2014 confirmed reproduction."}, {"tick": 6, "author": "Data team", "severity": "info", "text": "Our nightly Athena pipeline is failing \u2014 probably unrelated, ignore for now."}]};
|
| 67 |
+
|
| 68 |
+
// ── Network setup ───────────────────────────────���───────────────────
|
| 69 |
+
const STATUS_COLOR = {
|
| 70 |
+
healthy:'#3fb950', degraded:'#d29922', memory_leak:'#f85149',
|
| 71 |
+
cpu_throttled:'#db61a2', dead:'#6e7681', corrupted:'#a371f7',
|
| 72 |
+
rebuilding:'#58a6ff', rolling_back:'#f0883e'
|
| 73 |
+
};
|
| 74 |
+
const nodes = new vis.DataSet(REPLAY.nodes.map(n => ({
|
| 75 |
+
id: n, label: n, color: STATUS_COLOR.healthy, font:{color:'#fff'},
|
| 76 |
+
shape:'dot', size:14
|
| 77 |
+
})));
|
| 78 |
+
const edges = new vis.DataSet(REPLAY.edges.map(([a,b]) => ({
|
| 79 |
+
from: a, to: b, arrows:'to', color:'#30363d'
|
| 80 |
+
})));
|
| 81 |
+
const net = new vis.Network(document.getElementById('network'),
|
| 82 |
+
{nodes, edges},
|
| 83 |
+
{physics:{stabilization:true},
|
| 84 |
+
interaction:{hover:true},
|
| 85 |
+
nodes:{borderWidth:1.5}
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
// ── Chart ───────────────────────────────────────────────────────────
|
| 89 |
+
const ctx = document.getElementById('metricChart');
|
| 90 |
+
const chart = new Chart(ctx, {
|
| 91 |
+
type:'line',
|
| 92 |
+
data:{
|
| 93 |
+
labels: REPLAY.frames.map(f => 'tick '+f.tick),
|
| 94 |
+
datasets:[
|
| 95 |
+
{label:'mean error rate', data: REPLAY.frames.map(f => f.mean_err),
|
| 96 |
+
borderColor:'#f85149', tension:0.25},
|
| 97 |
+
{label:'mean latency (ms)', data: REPLAY.frames.map(f => f.mean_lat),
|
| 98 |
+
borderColor:'#d29922', tension:0.25, yAxisID:'y2'},
|
| 99 |
+
{label:'reward', data: REPLAY.frames.map(f => f.reward),
|
| 100 |
+
borderColor:'#3fb950', tension:0.25},
|
| 101 |
+
]
|
| 102 |
+
},
|
| 103 |
+
options:{
|
| 104 |
+
plugins:{legend:{labels:{color:'#c9d1d9'}}},
|
| 105 |
+
scales:{
|
| 106 |
+
x:{ticks:{color:'#8b949e'}},
|
| 107 |
+
y:{ticks:{color:'#8b949e'}, beginAtZero:true},
|
| 108 |
+
y2:{position:'right', ticks:{color:'#8b949e'}, grid:{drawOnChartArea:false}}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
// ── Tick scrubber ───────────────────────────────────────────────────
|
| 114 |
+
const evList = document.getElementById('evlist');
|
| 115 |
+
const slackList = document.getElementById('slacklist');
|
| 116 |
+
const tickLabel = document.getElementById('tickLabel');
|
| 117 |
+
const scrub = document.getElementById('scrub');
|
| 118 |
+
|
| 119 |
+
function applyTick(i) {
|
| 120 |
+
const f = REPLAY.frames[i];
|
| 121 |
+
if (!f) return;
|
| 122 |
+
// recolour nodes
|
| 123 |
+
Object.entries(f.statuses).forEach(([name, st]) => {
|
| 124 |
+
nodes.update({id:name, color: STATUS_COLOR[st] || '#6e7681'});
|
| 125 |
+
});
|
| 126 |
+
// events up to this tick
|
| 127 |
+
evList.innerHTML = REPLAY.events.filter(e => e.tick <= f.tick)
|
| 128 |
+
.slice(-20).map(e =>
|
| 129 |
+
`<div class="ev ${e.kind}"><b>[${e.kind}]</b> ${e.text}
|
| 130 |
+
<div class="meta">tick ${e.tick}</div></div>`).join('');
|
| 131 |
+
slackList.innerHTML = REPLAY.slack.filter(m => m.tick <= f.tick)
|
| 132 |
+
.slice(-12).map(m =>
|
| 133 |
+
`<div class="ev slack"><b>${m.author}:</b> ${m.text}
|
| 134 |
+
<div class="meta">[${m.severity}] tick ${m.tick}</div></div>`).join('');
|
| 135 |
+
tickLabel.textContent = `tick ${f.tick} / ${REPLAY.frames.length-1}`;
|
| 136 |
+
}
|
| 137 |
+
scrub.addEventListener('input', e => applyTick(+e.target.value));
|
| 138 |
+
applyTick(0);
|
| 139 |
+
|
| 140 |
+
// ── Auto-play ───────────────────────────────────────────────────────
|
| 141 |
+
let playing = false, timer = null;
|
| 142 |
+
const playBtn = document.getElementById('play');
|
| 143 |
+
playBtn.onclick = () => {
|
| 144 |
+
playing = !playing;
|
| 145 |
+
playBtn.textContent = playing ? '⏸ Pause' : '▶ Play';
|
| 146 |
+
playBtn.classList.toggle('pause', playing);
|
| 147 |
+
if (playing) {
|
| 148 |
+
timer = setInterval(() => {
|
| 149 |
+
let v = +scrub.value + 1;
|
| 150 |
+
if (v > REPLAY.frames.length-1) v = 0;
|
| 151 |
+
scrub.value = v;
|
| 152 |
+
applyTick(v);
|
| 153 |
+
}, 600);
|
| 154 |
+
} else {
|
| 155 |
+
clearInterval(timer);
|
| 156 |
+
}
|
| 157 |
+
};
|
| 158 |
+
</script>
|
| 159 |
+
</body>
|
| 160 |
+
</html>
|
rl-agent/scenarios/easy_cloudwatch_alarm_storm.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task19",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"target_score": 0.65,
|
| 5 |
+
"fault_type": "cloudwatch_alarm_storm",
|
| 6 |
+
"chaos_experiment_name": "alarm-storm-inject",
|
| 7 |
+
"ground_truth_root_cause": "cloudwatch_alarm_storm_from_chaos_inject",
|
| 8 |
+
"correct_mitigation_action": "delete_chaos_experiment",
|
| 9 |
+
"correct_mitigation_target": "alarm-storm-inject",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "inventory-service", "order-worker", "checkout-frontend"],
|
| 11 |
+
"useful_log_keywords": ["CloudWatch", "ALARM", "AlarmName", "StateChangeReason", "MetricName", "Threshold"],
|
| 12 |
+
"red_herrings": [],
|
| 13 |
+
"reference_postmortem": "Chaos experiment alarm-storm-inject is firing 47 fake CloudWatch alarms per minute via the PutMetricAlarm API, all of which fan out to the SNS alerts topic and PagerDuty, drowning out real signal. No actual service degradation. Mitigation: delete_chaos_experiment alarm-storm-inject. Follow-up: add an SNS subscription filter on AlarmName != 'chaos-*' so chaos noise never reaches PagerDuty, and a CloudWatch metric on AlarmsCreated/min."
|
| 14 |
+
}
|
rl-agent/scenarios/easy_lambda_cold_start.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task16",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"target_score": 0.65,
|
| 5 |
+
"fault_type": "lambda_cold_start",
|
| 6 |
+
"chaos_experiment_name": "lambda-cold-start-storm",
|
| 7 |
+
"ground_truth_root_cause": "lambda_cold_start_after_concurrency_quota_drop",
|
| 8 |
+
"correct_mitigation_action": "delete_chaos_experiment",
|
| 9 |
+
"correct_mitigation_target": "lambda-cold-start-storm",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api"],
|
| 11 |
+
"useful_log_keywords": ["INIT_REPORT", "Cold start", "ProvisionedConcurrencyUtilization", "Init Duration", "Lambda", "Throttled"],
|
| 12 |
+
"red_herrings": ["checkout-frontend"],
|
| 13 |
+
"reference_postmortem": "The notify-on-order Lambda backing notification-service's outbound email started reporting Init Duration > 4500ms because reserved concurrency was reduced from 50 to 5 by an earlier infra change. p99 latency on notification-service spiked. Chaos Mesh experiment lambda-cold-start-storm amplified this by triggering 200 cold starts. Mitigation: delete_chaos_experiment lambda-cold-start-storm. Follow-up: enable Provisioned Concurrency on this Lambda, add a CloudWatch alarm on Throttles > 0, and re-run capacity planning. Cost-awareness: Provisioned Concurrency costs ~$15/mo per instance — sized to peak."
|
| 14 |
+
}
|
rl-agent/scenarios/hard_bedrock_throttling.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task18",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"target_score": 0.40,
|
| 5 |
+
"fault_type": "bedrock_throttling",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "bedrock_invokemodel_throttled_by_account_quota",
|
| 8 |
+
"correct_mitigation_action": "scale_deployment",
|
| 9 |
+
"correct_mitigation_target": "notification-service",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "inventory-service"],
|
| 11 |
+
"useful_log_keywords": ["ThrottlingException", "Bedrock", "InvokeModel", "anthropic.claude", "TooManyRequestsException", "RetryAfter"],
|
| 12 |
+
"red_herrings": ["smtp"],
|
| 13 |
+
"reference_postmortem": "AWS Bedrock returned ThrottlingException on InvokeModel for anthropic.claude-3-haiku used by notification-service to summarise alerts. Account-level RPM quota = 60, the service was issuing 220 RPM during a marketing campaign. Mitigation: scale_deployment notification-service to fewer replicas (concurrency dropped) so per-instance request rate falls under the bucket; longer-term raise the quota. Follow-up: switch summarisation to a queue-batched async path with token-bucket rate limit, add a CloudWatch alarm on Bedrock-InvocationThrottles > 0, request a quota increase. Cost-awareness: smaller cheaper model (claude-3-haiku) at higher batch size is cheaper than claude-3-sonnet."
|
| 14 |
+
}
|
rl-agent/scenarios/hard_dynamodb_throttle.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task13",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"target_score": 0.40,
|
| 5 |
+
"fault_type": "dynamodb_throttle",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "dynamodb_provisioned_throughput_exceeded_on_inventory_table",
|
| 8 |
+
"correct_mitigation_action": "apply_config_patch",
|
| 9 |
+
"correct_mitigation_target": "inventory-service",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "order-worker"],
|
| 11 |
+
"useful_log_keywords": ["ProvisionedThroughputExceededException", "DynamoDB", "throttle", "ConditionalCheckFailed", "BatchGetItem", "inventory-table"],
|
| 12 |
+
"red_herrings": ["redis"],
|
| 13 |
+
"reference_postmortem": "DynamoDB returned ProvisionedThroughputExceededException at >40 rps on the inventory-items table because a Black-Friday traffic surge exceeded provisioned RCUs (50). inventory-service was retrying with exponential backoff but pod CPU climbed to 95%. Mitigation: apply_config_patch on inventory-service to switch the table to PAY_PER_REQUEST mode (set BILLING_MODE env var). Follow-up: enable DynamoDB auto-scaling on RCU/WCU, add a CloudWatch alarm on ConsumedReadCapacityUnits > 80% of provisioned, and instrument client-side metric for ThrottleCount. Cost-awareness: PAY_PER_REQUEST is cheaper for spikey workloads like ours."
|
| 14 |
+
}
|
rl-agent/scenarios/hard_eventbridge_silent_drop.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task20",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"target_score": 0.40,
|
| 5 |
+
"fault_type": "eventbridge_rule_silent_drop",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "eventbridge_rule_input_transformer_drops_events_silently",
|
| 8 |
+
"correct_mitigation_action": "rollback_deployment",
|
| 9 |
+
"correct_mitigation_target": "order-worker",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["notification-service", "payments-api"],
|
| 11 |
+
"useful_log_keywords": ["EventBridge", "PutEvents", "FailedEntryCount", "InputTransformer", "default-event-bus", "MatchedEvents", "InvocationsFailedToBeSentToDlq"],
|
| 12 |
+
"red_herrings": ["kafka"],
|
| 13 |
+
"reference_postmortem": "EventBridge rule order-events-to-worker has MatchedEvents=4200 but InvocationsFailedToBeSentToDlq=4200 — every order event is dropped silently. Root cause: order-worker v1.43 changed the event JSON schema and the EventBridge InputTransformer template still references $.detail.order.id which no longer exists (now $.detail.orderId). Mitigation: rollback_deployment on order-worker to v1.42 so the schema matches the InputTransformer. Follow-up: add a CloudWatch alarm on FailedInvocations > 0, version the event schema with a JSON-Schema contract, configure a DLQ on the rule. IAM: grant only events:PutEvents on the specific event bus, never *. Timeline T+0 deploy, T+02:30 first failure, T+09:00 alarm fired, T+12:00 mitigated."
|
| 14 |
+
}
|
rl-agent/scenarios/hard_kms_key_drift.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task23",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"target_score": 0.40,
|
| 5 |
+
"fault_type": "kms_key_pending_deletion",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "kms_data_key_scheduled_for_deletion_breaks_application_decrypt_path",
|
| 8 |
+
"correct_mitigation_action": "rollback_deployment",
|
| 9 |
+
"correct_mitigation_target": "payments-api",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["inventory-service", "order-worker"],
|
| 11 |
+
"useful_log_keywords": ["KMSInvalidStateException", "PendingDeletion", "alias/ic-data-key", "Decrypt", "AccessDeniedException", "ic-data-key"],
|
| 12 |
+
"red_herrings": ["smtp", "kafka"],
|
| 13 |
+
"reference_postmortem": "An operator mistakenly invoked schedule_key_deletion on alias/ic-data-key, the KMS CMK that payments-api uses to envelope-decrypt its at-rest tokenized PAN columns. The key entered KeyState=PendingDeletion with a 7-day window, and immediate Decrypt calls began returning KMSInvalidStateException. Mitigation: rollback_deployment payments-api to the prior revision pinned to the older key id while operations cancel_key_deletion + enable_key on the CMK. Follow-up: deny kms:ScheduleKeyDeletion in the org SCP except for break-glass roles, IRSA-scope IAM principals to cmk-specific Decrypt on alias/ic-data-key, audit CloudTrail for ScheduleKeyDeletion events, set up a CloudWatch alarm on KMS keystate change events. Cost-awareness: a single CMK with key rotation enabled is cheaper than per-service CMKs."
|
| 14 |
+
}
|
rl-agent/scenarios/hard_s3_iam_drift.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task15",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"target_score": 0.40,
|
| 5 |
+
"fault_type": "s3_403_iam_policy_drift",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "s3_bucket_policy_drift_blocking_inventory_putobject",
|
| 8 |
+
"correct_mitigation_action": "apply_config_patch",
|
| 9 |
+
"correct_mitigation_target": "inventory-service",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "order-worker", "notification-service"],
|
| 11 |
+
"useful_log_keywords": ["AccessDenied", "PutObject", "s3:PutObject", "AWS_S3_BUCKET", "SignatureDoesNotMatch", "IAM", "AssumeRole"],
|
| 12 |
+
"red_herrings": ["postgres", "kafka"],
|
| 13 |
+
"reference_postmortem": "inventory-service started getting 403 AccessDenied on s3:PutObject to ic-inventory-snapshots after a Terraform apply on infra/aws/main.tf removed the inventory-service-pod-role from the bucket policy's Principal list. Mitigation: apply_config_patch on inventory-service to point at the new IRSA role ARN that does have the s3:PutObject permission, OR re-add the role to the bucket policy. IAM least-privilege: ic-inventory-snapshots policy now scopes Action to s3:PutObject + s3:GetObject only (was s3:*) and Resource to /snapshots/* (was bucket-wide). Follow-up: add a CloudTrail alarm for bucket-policy changes."
|
| 14 |
+
}
|
rl-agent/scenarios/hard_step_functions_failure.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task21",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"target_score": 0.40,
|
| 5 |
+
"fault_type": "step_functions_execution_failure",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "step_functions_state_machine_execution_failed_due_to_unhandled_choice_default",
|
| 8 |
+
"correct_mitigation_action": "apply_config_patch",
|
| 9 |
+
"correct_mitigation_target": "order-worker",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "inventory-service"],
|
| 11 |
+
"useful_log_keywords": ["States.TaskFailed", "ExecutionFailed", "ic-order-saga", "Choice", "InducedFailure", "stateMachineArn"],
|
| 12 |
+
"red_herrings": ["smtp", "redis"],
|
| 13 |
+
"reference_postmortem": "Step Functions state machine ic-order-saga reached the Boom Fail state because the order-worker service emitted events with kind != 'happy'. The Choice state lacked a Default branch that retried with a sanitised payload, so all non-happy events terminated as ExecutionFailed. Mitigation: apply_config_patch on order-worker to set ORDER_KIND_DEFAULT=happy and add a Wait+Pass branch to the state machine. Follow-up: scope IAM least-privilege on the SFN execution role, add a CloudWatch alarm on ExecutionsFailed>0 wired to SNS, audit X-Ray traces for unhealthy executions. Cost-awareness: STANDARD vs EXPRESS — for short-lived order sagas, EXPRESS is cheaper at scale."
|
| 14 |
+
}
|
rl-agent/scenarios/medium_athena_failed_query.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task22",
|
| 3 |
+
"difficulty": "medium",
|
| 4 |
+
"target_score": 0.45,
|
| 5 |
+
"fault_type": "athena_failed_query",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "athena_query_failed_due_to_missing_glue_database_schema",
|
| 8 |
+
"correct_mitigation_action": "apply_config_patch",
|
| 9 |
+
"correct_mitigation_target": "inventory-service",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "order-worker"],
|
| 11 |
+
"useful_log_keywords": ["SCHEMA_NOT_FOUND", "Athena", "QueryExecution", "FAILED", "ic_chaos_nonexistent_db", "Glue"],
|
| 12 |
+
"red_herrings": ["s3", "kafka"],
|
| 13 |
+
"reference_postmortem": "Athena query submitted by inventory-service against ic_chaos_nonexistent_db landed in FAILED state with SCHEMA_NOT_FOUND from the Glue Data Catalog. Mitigation: apply_config_patch on inventory-service to point ATHENA_DATABASE to the canonical 'ic_inventory_metrics' database and add a pre-query Glue catalog presence check. Follow-up: add a CloudWatch alarm on Athena/QueryFailed metrics, IAM-scope inventory-service to glue:GetDatabase only on the canonical schema, audit s3://athena-results/ retention. Cost-awareness: workgroup-level data-scanned limits prevent runaway-cost queries; provisioned capacity pricing only when sustained QPS warrants it."
|
| 14 |
+
}
|
rl-agent/scenarios/medium_rds_connection_pool.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task17",
|
| 3 |
+
"difficulty": "medium",
|
| 4 |
+
"target_score": 0.50,
|
| 5 |
+
"fault_type": "rds_connection_pool_exhaustion",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "rds_connection_pool_exhaustion_from_long_running_transactions",
|
| 8 |
+
"correct_mitigation_action": "restart_pods",
|
| 9 |
+
"correct_mitigation_target": "payments-api",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["inventory-service", "order-worker"],
|
| 11 |
+
"useful_log_keywords": ["psycopg2.OperationalError", "remaining connection slots", "RDS", "DatabaseConnections", "idle in transaction", "max_connections"],
|
| 12 |
+
"red_herrings": ["redis", "kafka"],
|
| 13 |
+
"reference_postmortem": "RDS db.t3.medium reached max_connections=80 because payments-api leaked one DB connection per failed Stripe webhook (try/finally missing). Mitigation: restart_pods on payments-api closes the leaked sockets. Follow-up: add PgBouncer in transaction-pooling mode, fix the leak by wrapping the call in a context manager, add a CloudWatch alarm on DatabaseConnections > 80% of max. Cost-awareness: t3.medium is correctly sized for normal traffic; PgBouncer avoids upsizing. Security: rotate the IAM-auth DB password every 90 days."
|
| 14 |
+
}
|
rl-agent/scenarios/medium_secrets_manager_rotation.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task14",
|
| 3 |
+
"difficulty": "medium",
|
| 4 |
+
"target_score": 0.50,
|
| 5 |
+
"fault_type": "secrets_manager_rotation_lag",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "secrets_manager_rotation_did_not_propagate_to_payments_pods",
|
| 8 |
+
"correct_mitigation_action": "restart_pods",
|
| 9 |
+
"correct_mitigation_target": "payments-api",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["inventory-service", "checkout-frontend"],
|
| 11 |
+
"useful_log_keywords": ["Secrets Manager", "AccessDenied", "InvalidSignatureException", "RotationLambda", "stripe_api_key", "401 Unauthorized"],
|
| 12 |
+
"red_herrings": ["auth-service"],
|
| 13 |
+
"reference_postmortem": "AWS Secrets Manager rotated stripe_api_key on schedule, the rotation Lambda updated the secret version, but payments-api pods were caching the old SecretString in-memory and not re-fetching. All Stripe API calls returned 401 Unauthorized. Mitigation: restart_pods on payments-api so they re-read the secret. Follow-up: switch to External Secrets Operator with refreshInterval=60s, or subscribe pods to the AWSSecretsManagerRotation EventBridge rule. Security: never log the secret value."
|
| 14 |
+
}
|
rl-agent/scenarios/medium_sqs_dlq_growth.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task_id": "task12",
|
| 3 |
+
"difficulty": "medium",
|
| 4 |
+
"target_score": 0.50,
|
| 5 |
+
"fault_type": "sqs_dlq_growth",
|
| 6 |
+
"chaos_experiment_name": "",
|
| 7 |
+
"ground_truth_root_cause": "sqs_dlq_growth_from_payments_signature_mismatch",
|
| 8 |
+
"correct_mitigation_action": "rollback_deployment",
|
| 9 |
+
"correct_mitigation_target": "order-worker",
|
| 10 |
+
"wrong_mitigation_penalty_services": ["payments-api", "checkout-frontend"],
|
| 11 |
+
"useful_log_keywords": ["SQS", "ApproximateNumberOfMessages", "DLQ", "signature mismatch", "ReceiveMessage", "VisibilityTimeout"],
|
| 12 |
+
"red_herrings": ["notification-service"],
|
| 13 |
+
"reference_postmortem": "AWS SQS DLQ depth on the order-payments-dlq queue spiked from 0 to 12,400 within 4 minutes. Root cause: order-worker v1.42 introduced a new HMAC signature on the SQS message envelope that payments-api still rejects, so every message is retried 3x and dropped to DLQ. Mitigation: rollback order-worker to v1.41. Follow-up: add a contract test for the SQS envelope between order-worker and payments-api, and a CloudWatch alarm on ApproximateNumberOfMessages > 100 for the DLQ. IAM least-privilege: order-worker only needs sqs:SendMessage on the DLQ; remove sqs:* from the role."
|
| 14 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_101.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_101",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "checkout DynamoDB throttling",
|
| 5 |
+
"description": "`ic-checkout-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-checkout-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-checkout-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-checkout-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-checkout-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_102.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_102",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "orders DynamoDB throttling",
|
| 5 |
+
"description": "`ic-orders-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-orders-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-orders-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-orders-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-orders-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_103.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_103",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "inventory DynamoDB throttling",
|
| 5 |
+
"description": "`ic-inventory-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-inventory-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-inventory-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-inventory-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-inventory-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_104.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_104",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "payments DynamoDB throttling",
|
| 5 |
+
"description": "`ic-payments-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-payments-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-payments-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-payments-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-payments-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_105.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_105",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "search DynamoDB throttling",
|
| 5 |
+
"description": "`ic-search-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-search-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-search-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-search-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-search-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_106.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_106",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "recommendations DynamoDB throttling",
|
| 5 |
+
"description": "`ic-recommendations-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-recommendations-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-recommendations-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-recommendations-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-recommendations-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_107.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_107",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "auth DynamoDB throttling",
|
| 5 |
+
"description": "`ic-auth-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-auth-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-auth-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-auth-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-auth-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_108.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_108",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "billing DynamoDB throttling",
|
| 5 |
+
"description": "`ic-billing-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-billing-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-billing-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-billing-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-billing-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_109.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_109",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "shipping DynamoDB throttling",
|
| 5 |
+
"description": "`ic-shipping-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-shipping-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-shipping-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-shipping-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-shipping-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_110.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_110",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "notifications DynamoDB throttling",
|
| 5 |
+
"description": "`ic-notifications-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-notifications-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-notifications-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-notifications-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-notifications-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_111.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_111",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "reviews DynamoDB throttling",
|
| 5 |
+
"description": "`ic-reviews-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-reviews-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-reviews-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-reviews-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-reviews-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_112.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_112",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "catalog DynamoDB throttling",
|
| 5 |
+
"description": "`ic-catalog-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-catalog-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-catalog-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-catalog-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-catalog-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_113.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_113",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "fulfillment DynamoDB throttling",
|
| 5 |
+
"description": "`ic-fulfillment-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-fulfillment-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-fulfillment-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-fulfillment-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-fulfillment-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_114.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_114",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "telemetry DynamoDB throttling",
|
| 5 |
+
"description": "`ic-telemetry-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-telemetry-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-telemetry-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-telemetry-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-telemetry-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_115.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_115",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "analytics DynamoDB throttling",
|
| 5 |
+
"description": "`ic-analytics-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-analytics-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-analytics-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-analytics-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-analytics-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_116.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_116",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "userprofile DynamoDB throttling",
|
| 5 |
+
"description": "`ic-userprofile-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-userprofile-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-userprofile-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-userprofile-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-userprofile-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_117.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_117",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "cart DynamoDB throttling",
|
| 5 |
+
"description": "`ic-cart-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-cart-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-cart-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-cart-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-cart-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_118.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_118",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "pricing DynamoDB throttling",
|
| 5 |
+
"description": "`ic-pricing-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-pricing-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-pricing-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-pricing-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-pricing-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_119.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_119",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "promotions DynamoDB throttling",
|
| 5 |
+
"description": "`ic-promotions-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-promotions-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-promotions-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-promotions-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-promotions-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_ddb_throttle_120.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_ddb_throttle_120",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "media DynamoDB throttling",
|
| 5 |
+
"description": "`ic-media-table` is throttling writes. Scale write capacity.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "dynamodb/ic-media-table",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"name": "ic-media-table",
|
| 12 |
+
"items": {},
|
| 13 |
+
"throttled": true,
|
| 14 |
+
"capacity": 5
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [],
|
| 19 |
+
"correct_action_chain": [
|
| 20 |
+
{
|
| 21 |
+
"id": "dynamodb.describe",
|
| 22 |
+
"params": {
|
| 23 |
+
"resource_id": "ic-media-table"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "dynamodb.scale",
|
| 28 |
+
"params": {
|
| 29 |
+
"resource_id": "ic-media-table",
|
| 30 |
+
"capacity": 50
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"target_score": 0.55,
|
| 35 |
+
"max_steps": 16
|
| 36 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_061.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_061",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "checkout KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-checkout-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-checkout-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-checkout-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-checkout-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-checkout-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-checkout-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-checkout-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_062.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_062",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "orders KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-orders-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-orders-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-orders-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-orders-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-orders-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-orders-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-orders-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_063.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_063",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "inventory KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-inventory-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-inventory-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-inventory-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-inventory-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-inventory-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-inventory-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-inventory-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_064.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_064",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "payments KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-payments-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-payments-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-payments-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-payments-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-payments-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-payments-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-payments-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_065.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_065",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "search KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-search-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-search-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-search-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-search-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-search-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-search-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-search-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_066.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_066",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "recommendations KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-recommendations-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-recommendations-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-recommendations-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-recommendations-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-recommendations-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-recommendations-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-recommendations-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|
rl-agent/scenarios/sim/easy/sim_easy_kms_disabled_067.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sim_easy_kms_disabled_067",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"title": "auth KMS key disabled",
|
| 5 |
+
"description": "Encrypt calls fail with KMSInvalidStateException. Re-enable `alias/ic-auth-key`.",
|
| 6 |
+
"preconditions": [
|
| 7 |
+
{
|
| 8 |
+
"path": "kms/alias/ic-auth-key",
|
| 9 |
+
"op": "set",
|
| 10 |
+
"value": {
|
| 11 |
+
"alias": "alias/ic-auth-key",
|
| 12 |
+
"state": "Disabled",
|
| 13 |
+
"policy": "{}",
|
| 14 |
+
"pending_deletion_days": null
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"scheduled_failures": [
|
| 19 |
+
{
|
| 20 |
+
"action_id": "kms.encrypt",
|
| 21 |
+
"error": "KMSInvalidStateException",
|
| 22 |
+
"message": "key disabled",
|
| 23 |
+
"count": 1
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"correct_action_chain": [
|
| 27 |
+
{
|
| 28 |
+
"id": "kms.describe",
|
| 29 |
+
"params": {
|
| 30 |
+
"resource_id": "alias/ic-auth-key"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "kms.encrypt",
|
| 35 |
+
"params": {
|
| 36 |
+
"resource_id": "alias/ic-auth-key",
|
| 37 |
+
"plaintext": "hi"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "kms.enable",
|
| 42 |
+
"params": {
|
| 43 |
+
"resource_id": "alias/ic-auth-key"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "kms.encrypt",
|
| 48 |
+
"params": {
|
| 49 |
+
"resource_id": "alias/ic-auth-key",
|
| 50 |
+
"plaintext": "hi"
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"target_score": 0.55,
|
| 55 |
+
"max_steps": 16
|
| 56 |
+
}
|