mayhem-cli 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. mayhem/agent/__init__.py +1 -0
  2. mayhem/agent/cli.py +36 -0
  3. mayhem/agents/__init__.py +1 -0
  4. mayhem/agents/capabilities.py +106 -0
  5. mayhem/agents/executors.py +430 -0
  6. mayhem/agents/impact.py +729 -0
  7. mayhem/agents/lease_client.py +141 -0
  8. mayhem/agents/probes.py +284 -0
  9. mayhem/agents/protocol.py +134 -0
  10. mayhem/agents/server.py +281 -0
  11. mayhem/agents/sinks.py +60 -0
  12. mayhem/agents/transports.py +134 -0
  13. mayhem/agents/watchdog.py +140 -0
  14. mayhem/cli/__init__.py +11 -0
  15. mayhem/cli/app.py +154 -0
  16. mayhem/cli/campaign.py +496 -0
  17. mayhem/cli/config_cmd.py +47 -0
  18. mayhem/cli/context.py +23 -0
  19. mayhem/cli/dependency.py +429 -0
  20. mayhem/cli/exit_codes.py +24 -0
  21. mayhem/cli/experiment.py +24 -0
  22. mayhem/cli/lifecycle.py +805 -0
  23. mayhem/cli/resolver.py +72 -0
  24. mayhem/cli/services.py +459 -0
  25. mayhem/cli/style.py +101 -0
  26. mayhem/cli/toolkit.py +41 -0
  27. mayhem/cli/topology.py +127 -0
  28. mayhem/config.py +208 -0
  29. mayhem/controller/__init__.py +1 -0
  30. mayhem/controller/compensation.py +2156 -0
  31. mayhem/controller/executor.py +1719 -0
  32. mayhem/controller/janitor.py +196 -0
  33. mayhem/controller/observability_collector.py +382 -0
  34. mayhem/controller/observations.py +102 -0
  35. mayhem/controller/planner.py +715 -0
  36. mayhem/controller/recovery.py +245 -0
  37. mayhem/controller/resilience_report.py +585 -0
  38. mayhem/controller/resource_manager.py +457 -0
  39. mayhem/controller/safety.py +392 -0
  40. mayhem/domain/__init__.py +6 -0
  41. mayhem/domain/campaigns.py +118 -0
  42. mayhem/domain/cancellation.py +110 -0
  43. mayhem/domain/candidates.py +101 -0
  44. mayhem/domain/capabilities.py +86 -0
  45. mayhem/domain/catalog.py +727 -0
  46. mayhem/domain/checks.py +173 -0
  47. mayhem/domain/common.py +104 -0
  48. mayhem/domain/coverage.py +106 -0
  49. mayhem/domain/decisions.py +57 -0
  50. mayhem/domain/errors.py +87 -0
  51. mayhem/domain/events.py +61 -0
  52. mayhem/domain/execution_context.py +120 -0
  53. mayhem/domain/execution_loci.py +94 -0
  54. mayhem/domain/experiments.py +370 -0
  55. mayhem/domain/faults.py +239 -0
  56. mayhem/domain/identity.py +200 -0
  57. mayhem/domain/k8s_adapter.py +132 -0
  58. mayhem/domain/leases.py +186 -0
  59. mayhem/domain/load_strategy.py +98 -0
  60. mayhem/domain/m5_campaign.py +120 -0
  61. mayhem/domain/maniac.py +93 -0
  62. mayhem/domain/observability.py +146 -0
  63. mayhem/domain/outcomes.py +92 -0
  64. mayhem/domain/remote_agent_interface.py +70 -0
  65. mayhem/domain/resources.py +245 -0
  66. mayhem/domain/risks.py +61 -0
  67. mayhem/domain/run_outcome.py +146 -0
  68. mayhem/domain/runtime_adapter.py +256 -0
  69. mayhem/domain/success.py +329 -0
  70. mayhem/domain/topology.py +452 -0
  71. mayhem/infra/__init__.py +1 -0
  72. mayhem/infra/campaign_engine.py +205 -0
  73. mayhem/infra/candidate_gates.py +124 -0
  74. mayhem/infra/candidate_generator.py +110 -0
  75. mayhem/infra/coverage_repository.py +101 -0
  76. mayhem/infra/lease_repository.py +129 -0
  77. mayhem/infra/maniac.py +103 -0
  78. mayhem/infra/migrations.py +596 -0
  79. mayhem/infra/migrator.py +149 -0
  80. mayhem/infra/report.py +227 -0
  81. mayhem/infra/store.py +200 -0
  82. mayhem/py.typed +0 -0
  83. mayhem/spec.py +52 -0
  84. mayhem/toolkit/__init__.py +1 -0
  85. mayhem/toolkit/fingerprint.py +69 -0
  86. mayhem/toolkit/hashing.py +32 -0
  87. mayhem/toolkit/manifests/docker.yaml +11 -0
  88. mayhem/toolkit/manifests/podman.yaml +11 -0
  89. mayhem/toolkit/manifests/stress-ng.yaml +11 -0
  90. mayhem/toolkit/manifests/tc-netem.yaml +11 -0
  91. mayhem/toolkit/manifests/toxiproxy.yaml +10 -0
  92. mayhem/toolkit/registry.py +185 -0
  93. mayhem/toolkit/tool_runner.py +129 -0
  94. mayhem/topology/__init__.py +10 -0
  95. mayhem/topology/providers/__init__.py +0 -0
  96. mayhem/topology/providers/adapter_registry.py +60 -0
  97. mayhem/topology/providers/base.py +31 -0
  98. mayhem/topology/providers/compose.py +207 -0
  99. mayhem/topology/providers/docker_adapter.py +277 -0
  100. mayhem/topology/providers/docker_runtime.py +461 -0
  101. mayhem/topology/providers/podman_adapter.py +328 -0
  102. mayhem/topology/resolve.py +196 -0
  103. mayhem/topology/service.py +158 -0
  104. mayhem_cli-0.5.1.dist-info/METADATA +555 -0
  105. mayhem_cli-0.5.1.dist-info/RECORD +107 -0
  106. mayhem_cli-0.5.1.dist-info/WHEEL +4 -0
  107. mayhem_cli-0.5.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,727 @@
1
+ """Seed fault catalog — curated FaultDefinitions matching the taxonomy doc.
2
+
3
+ This is the planner's source of truth for what is injectable, at what risk,
4
+ on which node kinds, with which params. Backends gate actual tooling; a
5
+ definition without an executable compensation template will be refused at
6
+ plan time (see controller.compensation).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from mayhem.domain.capabilities import Capability
12
+ from mayhem.domain.faults import FaultCategory, FaultDefinition, ParamSpec, ParamType
13
+ from mayhem.domain.risks import RiskLevel
14
+ from mayhem.domain.topology import NodeKind
15
+
16
+ _S = ParamSpec(name="seconds", type=ParamType.DURATION)
17
+
18
+
19
+ def _pct(minimum: float, maximum: float) -> ParamSpec:
20
+ return ParamSpec(name="percent", type=ParamType.PERCENT, minimum=minimum, maximum=maximum)
21
+
22
+
23
+ CATALOG: tuple[FaultDefinition, ...] = (
24
+ FaultDefinition(
25
+ id="proc.pause",
26
+ category=FaultCategory.PROCESS,
27
+ risk=RiskLevel.LOW,
28
+ required_caps=frozenset({Capability.PROCESS_CONTROL}),
29
+ applicable_node_kinds=frozenset({NodeKind.PROCESS, NodeKind.SERVICE, NodeKind.CONTAINER}),
30
+ max_duration_s=600.0,
31
+ params_schema=(),
32
+ ),
33
+ FaultDefinition(
34
+ id="process.stop",
35
+ category=FaultCategory.PROCESS,
36
+ risk=RiskLevel.MEDIUM,
37
+ required_caps=frozenset({Capability.PROCESS_CONTROL}),
38
+ applicable_node_kinds=frozenset({NodeKind.PROCESS, NodeKind.SERVICE, NodeKind.CONTAINER}),
39
+ max_duration_s=300.0,
40
+ params_schema=(),
41
+ ),
42
+ FaultDefinition(
43
+ id="process.kill",
44
+ category=FaultCategory.PROCESS,
45
+ risk=RiskLevel.HIGH,
46
+ required_caps=frozenset({Capability.PROCESS_CONTROL}),
47
+ applicable_node_kinds=frozenset({NodeKind.PROCESS, NodeKind.SERVICE, NodeKind.CONTAINER}),
48
+ max_duration_s=60.0,
49
+ params_schema=(),
50
+ ),
51
+ FaultDefinition(
52
+ id="process.crash_loop",
53
+ category=FaultCategory.PROCESS,
54
+ risk=RiskLevel.HIGH,
55
+ required_caps=frozenset({Capability.DOCKER_ENGINE}),
56
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
57
+ max_duration_s=120.0,
58
+ params_schema=(
59
+ ParamSpec(name="restarts", type=ParamType.INTEGER, minimum=1, maximum=1000, default=10),
60
+ ParamSpec(name="interval", type=ParamType.STRING, default="2s"),
61
+ ),
62
+ ),
63
+ FaultDefinition(
64
+ id="cpu.saturate",
65
+ category=FaultCategory.CPU,
66
+ risk=RiskLevel.MEDIUM,
67
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.HOST}),
68
+ max_duration_s=300.0,
69
+ params_schema=(_pct(minimum=1.0, maximum=100.0),),
70
+ ),
71
+ FaultDefinition(
72
+ id="cpu.throttle",
73
+ category=FaultCategory.CPU,
74
+ risk=RiskLevel.MEDIUM,
75
+ required_caps=frozenset({Capability.DOCKER_ENGINE}),
76
+ # A reserve-share throttle needs the container engine cgroup knobs
77
+ # (``update --cpus``), so it cannot address a bare HOST process.
78
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
79
+ max_duration_s=300.0,
80
+ params_schema=(_pct(minimum=1.0, maximum=100.0),),
81
+ ),
82
+ FaultDefinition(
83
+ id="mem.exhaust",
84
+ category=FaultCategory.MEMORY,
85
+ risk=RiskLevel.HIGH,
86
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
87
+ max_duration_s=120.0,
88
+ params_schema=(
89
+ _pct(minimum=1.0, maximum=99.0),
90
+ ParamSpec(name="amount", type=ParamType.BYTES),
91
+ # ``allocate`` (default) commits anonymous memory. A slow, sustained
92
+ # leak is a distinct fault (mem.leak); this fault only ever allocates,
93
+ # so any other mode is refused at plan time.
94
+ ParamSpec(name="mode", type=ParamType.STRING, default="allocate"),
95
+ ),
96
+ ),
97
+ FaultDefinition(
98
+ id="mem.leak",
99
+ category=FaultCategory.MEMORY,
100
+ risk=RiskLevel.HIGH,
101
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
102
+ max_duration_s=300.0,
103
+ params_schema=(
104
+ ParamSpec(name="rate_mb", type=ParamType.INTEGER, default=8, minimum=1, maximum=512),
105
+ ),
106
+ ),
107
+ FaultDefinition(
108
+ id="fs.fill",
109
+ category=FaultCategory.STORAGE,
110
+ risk=RiskLevel.MEDIUM,
111
+ reversible=True,
112
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.HOST}),
113
+ max_duration_s=300.0,
114
+ params_schema=(_pct(minimum=1.0, maximum=99.0),),
115
+ ),
116
+ FaultDefinition(
117
+ id="fs.inode_exhaust",
118
+ category=FaultCategory.STORAGE,
119
+ risk=RiskLevel.MEDIUM,
120
+ reversible=True,
121
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.HOST}),
122
+ max_duration_s=300.0,
123
+ # Consumes free inodes (zero-byte marker files) instead of capacity;
124
+ # undo removes the marker files, so the filesystem is fully restored.
125
+ params_schema=(_pct(minimum=1.0, maximum=99.0),),
126
+ ),
127
+ FaultDefinition(
128
+ id="fs.io_stress",
129
+ category=FaultCategory.STORAGE,
130
+ risk=RiskLevel.MEDIUM,
131
+ reversible=True,
132
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.HOST}),
133
+ max_duration_s=120.0,
134
+ params_schema=(
135
+ _S,
136
+ ParamSpec(name="workers", type=ParamType.INTEGER, default=1, minimum=1, maximum=8),
137
+ ParamSpec(
138
+ name="io_bytes",
139
+ type=ParamType.BYTES,
140
+ default=64 * 1024 * 1024,
141
+ minimum=1024 * 1024,
142
+ maximum=1024**3,
143
+ ),
144
+ # Spec-compliant throughput twins: when either is set the payload
145
+ # drives sustained read+write I/O at the given MiB/s per worker
146
+ # instead of the io_bytes churn loop.
147
+ ParamSpec(name="read_mb_s", type=ParamType.INTEGER, minimum=1, maximum=512),
148
+ ParamSpec(name="write_mb_s", type=ParamType.INTEGER, minimum=1, maximum=512),
149
+ ParamSpec(name="block_size", type=ParamType.STRING, default="64k"),
150
+ ),
151
+ ),
152
+ FaultDefinition(
153
+ id="fs.read_only",
154
+ category=FaultCategory.STORAGE,
155
+ risk=RiskLevel.HIGH,
156
+ reversible=True,
157
+ required_caps=frozenset({Capability.FS_CONTROL}),
158
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.HOST}),
159
+ max_duration_s=120.0,
160
+ params_schema=(ParamSpec(name="path", type=ParamType.STRING, default="/"),),
161
+ ),
162
+ FaultDefinition(
163
+ id="net.latency",
164
+ category=FaultCategory.NETWORK,
165
+ risk=RiskLevel.MEDIUM,
166
+ required_caps=frozenset({Capability.NET_ADMIN}),
167
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
168
+ max_duration_s=300.0,
169
+ params_schema=(
170
+ _S,
171
+ ParamSpec(name="jitter_ms", type=ParamType.INTEGER, default=0),
172
+ ParamSpec(name="direction", type=ParamType.STRING, default="egress"),
173
+ ),
174
+ ),
175
+ FaultDefinition(
176
+ id="net.packet_loss",
177
+ category=FaultCategory.NETWORK,
178
+ risk=RiskLevel.MEDIUM,
179
+ required_caps=frozenset({Capability.NET_ADMIN}),
180
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
181
+ max_duration_s=300.0,
182
+ params_schema=(
183
+ _pct(minimum=1.0, maximum=100.0),
184
+ ParamSpec(name="direction", type=ParamType.STRING, default="egress"),
185
+ ),
186
+ ),
187
+ FaultDefinition(
188
+ id="net.bandwidth",
189
+ category=FaultCategory.NETWORK,
190
+ risk=RiskLevel.MEDIUM,
191
+ required_caps=frozenset({Capability.NET_ADMIN}),
192
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
193
+ max_duration_s=300.0,
194
+ params_schema=(
195
+ ParamSpec(name="rate", type=ParamType.STRING, required=True),
196
+ ParamSpec(name="burst", type=ParamType.STRING, default="10k"),
197
+ ParamSpec(name="direction", type=ParamType.STRING, default="egress"),
198
+ ),
199
+ ),
200
+ FaultDefinition(
201
+ id="net.partition",
202
+ category=FaultCategory.NETWORK,
203
+ risk=RiskLevel.HIGH,
204
+ required_caps=frozenset({Capability.NET_ADMIN}),
205
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
206
+ max_duration_s=120.0,
207
+ params_schema=(),
208
+ ),
209
+ FaultDefinition(
210
+ id="net.load",
211
+ category=FaultCategory.NETWORK,
212
+ risk=RiskLevel.MEDIUM,
213
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
214
+ max_duration_s=600.0,
215
+ params_schema=(
216
+ ParamSpec(name="users", type=ParamType.INTEGER, minimum=1.0),
217
+ # ``url``: target for the generated load script. Defaults to the
218
+ # target container's own network address (live IP + first TCP
219
+ # serving port); ``http://localhost/`` only when no address is
220
+ # known at plan time.
221
+ ParamSpec(name="url", type=ParamType.STRING, default=None),
222
+ # ``script``: path on the drill host to a k6 ``script.js`` run by
223
+ # the host k6 against the target; when set it replaces the built-in
224
+ # inline script instead of ``url``.
225
+ ParamSpec(name="script", type=ParamType.STRING, default=None),
226
+ ),
227
+ ),
228
+ FaultDefinition(
229
+ id="net.connection_reset",
230
+ category=FaultCategory.NETWORK,
231
+ risk=RiskLevel.MEDIUM,
232
+ required_caps=frozenset({Capability.NET_ADMIN}),
233
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
234
+ max_duration_s=300.0,
235
+ params_schema=(
236
+ ParamSpec(name="port", type=ParamType.INTEGER, required=True, minimum=1, maximum=65535),
237
+ ParamSpec(name="protocol", type=ParamType.STRING, default="tcp"),
238
+ ),
239
+ ),
240
+ FaultDefinition(
241
+ id="net.connection_refuse",
242
+ category=FaultCategory.NETWORK,
243
+ risk=RiskLevel.HIGH,
244
+ required_caps=frozenset({Capability.NET_ADMIN}),
245
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
246
+ max_duration_s=300.0,
247
+ params_schema=(
248
+ ParamSpec(name="port", type=ParamType.INTEGER, required=True, minimum=1, maximum=65535),
249
+ ParamSpec(name="protocol", type=ParamType.STRING, default="tcp"),
250
+ ),
251
+ ),
252
+ FaultDefinition(
253
+ id="net.reorder",
254
+ category=FaultCategory.NETWORK,
255
+ risk=RiskLevel.MEDIUM,
256
+ required_caps=frozenset({Capability.NET_ADMIN}),
257
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
258
+ max_duration_s=300.0,
259
+ params_schema=(
260
+ _pct(minimum=1.0, maximum=100.0),
261
+ ParamSpec(name="delay_ms", type=ParamType.INTEGER, default=50),
262
+ ParamSpec(name="direction", type=ParamType.STRING, default="egress"),
263
+ ),
264
+ ),
265
+ FaultDefinition(
266
+ id="net.duplicate",
267
+ category=FaultCategory.NETWORK,
268
+ risk=RiskLevel.MEDIUM,
269
+ required_caps=frozenset({Capability.NET_ADMIN}),
270
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
271
+ max_duration_s=300.0,
272
+ params_schema=(
273
+ _pct(minimum=1.0, maximum=100.0),
274
+ ParamSpec(name="direction", type=ParamType.STRING, default="egress"),
275
+ ),
276
+ ),
277
+ FaultDefinition(
278
+ id="node.service_stop",
279
+ category=FaultCategory.NODE,
280
+ risk=RiskLevel.HIGH,
281
+ applicable_node_kinds=frozenset({NodeKind.SERVICE}),
282
+ max_duration_s=120.0,
283
+ params_schema=(),
284
+ ),
285
+ FaultDefinition(
286
+ id="http.error_injection",
287
+ category=FaultCategory.HTTP_API,
288
+ risk=RiskLevel.MEDIUM,
289
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.EXTERNAL_DEPENDENCY}),
290
+ max_duration_s=300.0,
291
+ params_schema=(
292
+ ParamSpec(name="status", type=ParamType.INTEGER, default=500),
293
+ ParamSpec(
294
+ name="probability",
295
+ type=ParamType.PERCENT,
296
+ minimum=0.0,
297
+ maximum=100.0,
298
+ default=0.0,
299
+ ),
300
+ ParamSpec(
301
+ name="port",
302
+ type=ParamType.INTEGER,
303
+ minimum=1,
304
+ maximum=65535,
305
+ default=80,
306
+ ),
307
+ ),
308
+ ),
309
+ FaultDefinition(
310
+ id="http.latency",
311
+ category=FaultCategory.HTTP_API,
312
+ risk=RiskLevel.MEDIUM,
313
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.EXTERNAL_DEPENDENCY}),
314
+ max_duration_s=300.0,
315
+ params_schema=(
316
+ ParamSpec(name="delay_ms", type=ParamType.INTEGER, minimum=1, maximum=30000),
317
+ ParamSpec(
318
+ name="probability",
319
+ type=ParamType.PERCENT,
320
+ minimum=1.0,
321
+ maximum=100.0,
322
+ default=100.0,
323
+ ),
324
+ ParamSpec(
325
+ name="port",
326
+ type=ParamType.INTEGER,
327
+ minimum=1,
328
+ maximum=65535,
329
+ default=80,
330
+ ),
331
+ ),
332
+ ),
333
+ FaultDefinition(
334
+ id="db.slow_query",
335
+ category=FaultCategory.DATABASE,
336
+ risk=RiskLevel.MEDIUM,
337
+ applicable_node_kinds=frozenset({NodeKind.EXTERNAL_DEPENDENCY, NodeKind.SERVICE}),
338
+ max_duration_s=300.0,
339
+ params_schema=(_S,),
340
+ ),
341
+ FaultDefinition(
342
+ id="db.connection_exhaust",
343
+ category=FaultCategory.DATABASE,
344
+ risk=RiskLevel.HIGH,
345
+ applicable_node_kinds=frozenset(
346
+ {NodeKind.EXTERNAL_DEPENDENCY, NodeKind.SERVICE, NodeKind.CONTAINER}
347
+ ),
348
+ max_duration_s=120.0,
349
+ params_schema=(
350
+ ParamSpec(
351
+ name="connections", type=ParamType.INTEGER, required=True, minimum=1, maximum=256
352
+ ),
353
+ ParamSpec(name="host", type=ParamType.STRING, required=True),
354
+ ParamSpec(
355
+ name="port",
356
+ type=ParamType.INTEGER,
357
+ minimum=1,
358
+ maximum=65535,
359
+ default=3306,
360
+ ),
361
+ ),
362
+ ),
363
+ FaultDefinition(
364
+ id="db.query_error",
365
+ category=FaultCategory.DATABASE,
366
+ risk=RiskLevel.HIGH,
367
+ required_caps=frozenset({Capability.NET_ADMIN}),
368
+ applicable_node_kinds=frozenset(
369
+ {NodeKind.EXTERNAL_DEPENDENCY, NodeKind.SERVICE, NodeKind.CONTAINER}
370
+ ),
371
+ max_duration_s=300.0,
372
+ params_schema=(
373
+ ParamSpec(
374
+ name="probability",
375
+ type=ParamType.PERCENT,
376
+ minimum=1.0,
377
+ maximum=100.0,
378
+ default=100.0,
379
+ ),
380
+ ParamSpec(name="error", type=ParamType.STRING, default="deadlock"),
381
+ ParamSpec(
382
+ name="port",
383
+ type=ParamType.INTEGER,
384
+ minimum=1,
385
+ maximum=65535,
386
+ default=3306,
387
+ ),
388
+ ),
389
+ ),
390
+ FaultDefinition(
391
+ id="load.spike",
392
+ category=FaultCategory.LOAD,
393
+ risk=RiskLevel.LOW,
394
+ applicable_node_kinds=frozenset({NodeKind.SERVICE}),
395
+ max_duration_s=900.0,
396
+ params_schema=(
397
+ ParamSpec(name="rps", type=ParamType.INTEGER, minimum=1.0),
398
+ _S,
399
+ ),
400
+ ),
401
+ FaultDefinition(
402
+ id="fuzz.protocol_abuse",
403
+ category=FaultCategory.FUZZ,
404
+ risk=RiskLevel.HIGH,
405
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.EXTERNAL_DEPENDENCY}),
406
+ max_duration_s=180.0,
407
+ params_schema=(),
408
+ ),
409
+ FaultDefinition(
410
+ id="dns.resolve_delay",
411
+ category=FaultCategory.DNS,
412
+ risk=RiskLevel.MEDIUM,
413
+ required_caps=frozenset({Capability.NET_ADMIN}),
414
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.HOST}),
415
+ max_duration_s=300.0,
416
+ params_schema=(_S,),
417
+ ),
418
+ FaultDefinition(
419
+ id="dns.timeout",
420
+ category=FaultCategory.DNS,
421
+ risk=RiskLevel.HIGH,
422
+ required_caps=frozenset({Capability.NET_ADMIN}),
423
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.HOST}),
424
+ max_duration_s=120.0,
425
+ params_schema=(),
426
+ ),
427
+ FaultDefinition(
428
+ id="dns.servfail",
429
+ category=FaultCategory.DNS,
430
+ risk=RiskLevel.MEDIUM,
431
+ required_caps=frozenset({Capability.NET_ADMIN}),
432
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.HOST}),
433
+ max_duration_s=120.0,
434
+ params_schema=(),
435
+ ),
436
+ FaultDefinition(
437
+ id="dns.nxdomain",
438
+ category=FaultCategory.DNS,
439
+ risk=RiskLevel.HIGH,
440
+ required_caps=frozenset({Capability.NET_ADMIN}),
441
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.HOST}),
442
+ max_duration_s=300.0,
443
+ params_schema=(ParamSpec(name="domain", type=ParamType.STRING),),
444
+ ),
445
+ FaultDefinition(
446
+ id="tls.certificate_expired",
447
+ category=FaultCategory.TLS,
448
+ risk=RiskLevel.HIGH,
449
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.EXTERNAL_DEPENDENCY}),
450
+ max_duration_s=120.0,
451
+ params_schema=(),
452
+ ),
453
+ FaultDefinition(
454
+ id="tls.handshake_failure",
455
+ category=FaultCategory.TLS,
456
+ risk=RiskLevel.HIGH,
457
+ required_caps=frozenset({Capability.NET_ADMIN}),
458
+ applicable_node_kinds=frozenset(
459
+ {NodeKind.SERVICE, NodeKind.EXTERNAL_DEPENDENCY, NodeKind.CONTAINER}
460
+ ),
461
+ max_duration_s=120.0,
462
+ params_schema=(
463
+ ParamSpec(
464
+ name="port",
465
+ type=ParamType.INTEGER,
466
+ minimum=1,
467
+ maximum=65535,
468
+ default=443,
469
+ ),
470
+ ),
471
+ ),
472
+ FaultDefinition(
473
+ id="container.kill",
474
+ category=FaultCategory.CONTAINER,
475
+ risk=RiskLevel.MEDIUM,
476
+ required_caps=frozenset({Capability.DOCKER_ENGINE}),
477
+ # A compose service is a SERVICE-kind node backed by a container whose
478
+ # container.kill restarts, so accept SERVICE like sibling faults.
479
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
480
+ max_duration_s=60.0,
481
+ params_schema=(ParamSpec(name="signal", type=ParamType.STRING, default="SIGKILL"),),
482
+ ),
483
+ FaultDefinition(
484
+ id="container.restart",
485
+ category=FaultCategory.CONTAINER,
486
+ risk=RiskLevel.MEDIUM,
487
+ required_caps=frozenset({Capability.DOCKER_ENGINE}),
488
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
489
+ max_duration_s=60.0,
490
+ params_schema=(),
491
+ ),
492
+ FaultDefinition(
493
+ id="container.pause",
494
+ category=FaultCategory.CONTAINER,
495
+ risk=RiskLevel.MEDIUM,
496
+ required_caps=frozenset({Capability.DOCKER_ENGINE}),
497
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER}),
498
+ max_duration_s=300.0,
499
+ params_schema=(),
500
+ ),
501
+ FaultDefinition(
502
+ id="clock.skew",
503
+ category=FaultCategory.CLOCK,
504
+ risk=RiskLevel.HIGH,
505
+ required_caps=frozenset({Capability.NET_ADMIN}),
506
+ applicable_node_kinds=frozenset({NodeKind.HOST, NodeKind.CONTAINER, NodeKind.SERVICE}),
507
+ max_duration_s=300.0,
508
+ params_schema=(ParamSpec(name="offset_ms", type=ParamType.INTEGER, required=True),),
509
+ ),
510
+ FaultDefinition(
511
+ id="dependency.block",
512
+ category=FaultCategory.DEPENDENCY,
513
+ risk=RiskLevel.HIGH,
514
+ required_caps=frozenset({Capability.NET_ADMIN}),
515
+ applicable_node_kinds=frozenset(
516
+ {NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.EXTERNAL_DEPENDENCY}
517
+ ),
518
+ max_duration_s=300.0,
519
+ params_schema=(
520
+ ParamSpec(name="port", type=ParamType.INTEGER, required=True, minimum=1, maximum=65535),
521
+ ParamSpec(name="protocol", type=ParamType.STRING, default="tcp"),
522
+ ),
523
+ ),
524
+ FaultDefinition(
525
+ id="dependency.timeout",
526
+ category=FaultCategory.DEPENDENCY,
527
+ risk=RiskLevel.MEDIUM,
528
+ required_caps=frozenset({Capability.NET_ADMIN}),
529
+ applicable_node_kinds=frozenset(
530
+ {NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.EXTERNAL_DEPENDENCY}
531
+ ),
532
+ max_duration_s=300.0,
533
+ params_schema=(
534
+ ParamSpec(name="port", type=ParamType.INTEGER, required=True, minimum=1, maximum=65535),
535
+ ParamSpec(
536
+ name="delay_ms",
537
+ type=ParamType.INTEGER,
538
+ required=True,
539
+ minimum=1,
540
+ maximum=30000,
541
+ ),
542
+ ParamSpec(name="protocol", type=ParamType.STRING, default="tcp"),
543
+ ),
544
+ ),
545
+ FaultDefinition(
546
+ id="dependency.flap",
547
+ category=FaultCategory.DEPENDENCY,
548
+ risk=RiskLevel.HIGH,
549
+ required_caps=frozenset({Capability.NET_ADMIN}),
550
+ applicable_node_kinds=frozenset(
551
+ {NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.EXTERNAL_DEPENDENCY}
552
+ ),
553
+ max_duration_s=300.0,
554
+ params_schema=(
555
+ ParamSpec(name="port", type=ParamType.INTEGER, required=True, minimum=1, maximum=65535),
556
+ ParamSpec(name="interval", type=ParamType.DURATION, default=10.0),
557
+ ParamSpec(name="failure_probability", type=ParamType.PERCENT, default=50.0),
558
+ ParamSpec(name="protocol", type=ParamType.STRING, default="tcp"),
559
+ ),
560
+ ),
561
+ FaultDefinition(
562
+ id="dependency.rate_limit",
563
+ category=FaultCategory.DEPENDENCY,
564
+ risk=RiskLevel.MEDIUM,
565
+ applicable_node_kinds=frozenset(
566
+ {NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.EXTERNAL_DEPENDENCY}
567
+ ),
568
+ max_duration_s=300.0,
569
+ params_schema=(
570
+ ParamSpec(
571
+ name="rate",
572
+ type=ParamType.INTEGER,
573
+ required=True,
574
+ minimum=1,
575
+ maximum=1000,
576
+ ),
577
+ ParamSpec(name="burst", type=ParamType.INTEGER, default=200, minimum=1, maximum=1000),
578
+ ParamSpec(name="code", type=ParamType.INTEGER, default=429),
579
+ ParamSpec(
580
+ name="port",
581
+ type=ParamType.INTEGER,
582
+ minimum=1,
583
+ maximum=65535,
584
+ default=80,
585
+ ),
586
+ ),
587
+ ),
588
+ FaultDefinition(
589
+ id="dependency.connection_refuse",
590
+ category=FaultCategory.DEPENDENCY,
591
+ risk=RiskLevel.HIGH,
592
+ required_caps=frozenset({Capability.NET_ADMIN}),
593
+ applicable_node_kinds=frozenset(
594
+ {NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.EXTERNAL_DEPENDENCY}
595
+ ),
596
+ max_duration_s=300.0,
597
+ params_schema=(
598
+ ParamSpec(name="port", type=ParamType.INTEGER, required=True, minimum=1, maximum=65535),
599
+ ParamSpec(name="protocol", type=ParamType.STRING, default="tcp"),
600
+ ),
601
+ ),
602
+ FaultDefinition(
603
+ id="fd.exhaust",
604
+ category=FaultCategory.FD,
605
+ risk=RiskLevel.HIGH,
606
+ applicable_node_kinds=frozenset({NodeKind.SERVICE, NodeKind.CONTAINER, NodeKind.HOST}),
607
+ max_duration_s=120.0,
608
+ params_schema=(ParamSpec(name="limit", type=ParamType.INTEGER, default=64),),
609
+ ),
610
+ # ── Kubernetes archetypes (ADR-M7-3, ADR-M7-4) ──────────────────────────
611
+ # All k8s archetypes are UNSUPPORTED (no live cluster driver); they exist so
612
+ # the planner + capability matrix can reason about k8s targets without
613
+ # executing. Fingerprints reuse M2/M3 patterns for future k8s drivers.
614
+ #
615
+ # Capacity-stress sub-category (ADR-M7-3)
616
+ FaultDefinition(
617
+ id="k8s.node_pressure",
618
+ category=FaultCategory.K8S,
619
+ risk=RiskLevel.HIGH,
620
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
621
+ applicable_node_kinds=frozenset({NodeKind.K8S_NODE}),
622
+ max_duration_s=300.0,
623
+ params_schema=(
624
+ ParamSpec(name="resource", type=ParamType.STRING, default="cpu"),
625
+ ParamSpec(name="target_percent", type=ParamType.PERCENT, minimum=1, maximum=100),
626
+ ),
627
+ ),
628
+ FaultDefinition(
629
+ id="k8s.pod_oom",
630
+ category=FaultCategory.K8S,
631
+ risk=RiskLevel.MEDIUM,
632
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
633
+ applicable_node_kinds=frozenset({NodeKind.POD}),
634
+ max_duration_s=120.0,
635
+ params_schema=(ParamSpec(name="memory_limit", type=ParamType.STRING, default="64Mi"),),
636
+ ),
637
+ FaultDefinition(
638
+ id="k8s.pod_pressure",
639
+ category=FaultCategory.K8S,
640
+ risk=RiskLevel.MEDIUM,
641
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
642
+ applicable_node_kinds=frozenset({NodeKind.POD}),
643
+ max_duration_s=300.0,
644
+ params_schema=(
645
+ ParamSpec(name="resource", type=ParamType.STRING, default="cpu"),
646
+ ParamSpec(name="target_percent", type=ParamType.PERCENT, minimum=1, maximum=100),
647
+ ),
648
+ ),
649
+ # Network sub-category (ADR-M7-3)
650
+ FaultDefinition(
651
+ id="k8s.network_policy",
652
+ category=FaultCategory.K8S,
653
+ risk=RiskLevel.HIGH,
654
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
655
+ applicable_node_kinds=frozenset({NodeKind.POD, NodeKind.K8S_NODE}),
656
+ max_duration_s=300.0,
657
+ params_schema=(
658
+ ParamSpec(name="policy_name", type=ParamType.STRING),
659
+ ParamSpec(name="direction", type=ParamType.STRING, default="ingress"),
660
+ ),
661
+ ),
662
+ FaultDefinition(
663
+ id="k8s.pod_latency",
664
+ category=FaultCategory.K8S,
665
+ risk=RiskLevel.MEDIUM,
666
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
667
+ applicable_node_kinds=frozenset({NodeKind.POD}),
668
+ max_duration_s=300.0,
669
+ params_schema=(
670
+ _S,
671
+ ParamSpec(name="jitter_ms", type=ParamType.FLOAT, minimum=0, maximum=5000),
672
+ ),
673
+ ),
674
+ FaultDefinition(
675
+ id="k8s.pod_partition",
676
+ category=FaultCategory.K8S,
677
+ risk=RiskLevel.HIGH,
678
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
679
+ applicable_node_kinds=frozenset({NodeKind.POD}),
680
+ max_duration_s=300.0,
681
+ params_schema=(_S,),
682
+ ),
683
+ # Preemption sub-category (ADR-M7-3)
684
+ FaultDefinition(
685
+ id="k8s.pod_evict",
686
+ category=FaultCategory.K8S,
687
+ risk=RiskLevel.HIGH,
688
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
689
+ applicable_node_kinds=frozenset({NodeKind.POD}),
690
+ max_duration_s=120.0,
691
+ params_schema=(),
692
+ ),
693
+ FaultDefinition(
694
+ id="k8s.pod_kill",
695
+ category=FaultCategory.K8S,
696
+ risk=RiskLevel.HIGH,
697
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
698
+ applicable_node_kinds=frozenset({NodeKind.POD}),
699
+ max_duration_s=60.0,
700
+ params_schema=(),
701
+ ),
702
+ FaultDefinition(
703
+ id="k8s.node_drain",
704
+ category=FaultCategory.K8S,
705
+ risk=RiskLevel.CRITICAL,
706
+ required_caps=frozenset({Capability.KUBERNETES_ENGINE}),
707
+ applicable_node_kinds=frozenset({NodeKind.K8S_NODE}),
708
+ max_duration_s=600.0,
709
+ params_schema=(ParamSpec(name="grace_period", type=ParamType.INTEGER, default=30),),
710
+ ),
711
+ )
712
+
713
+ _BY_ID: dict[str, FaultDefinition] = {d.id: d for d in CATALOG}
714
+
715
+
716
+ def definition_for(fault_id: str) -> FaultDefinition:
717
+ """Catalog lookup; unknown ids are planning errors, not KeyErrors."""
718
+ try:
719
+ return _BY_ID[fault_id]
720
+ except KeyError:
721
+ known = ", ".join(sorted(_BY_ID))
722
+ msg = f"fault {fault_id!r} not in catalog (known: {known})"
723
+ raise LookupError(msg) from None
724
+
725
+
726
+ def all_definitions() -> tuple[FaultDefinition, ...]:
727
+ return CATALOG