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,101 @@
1
+ """ExperimentCandidate — ADR-M5-2 (M5 Phase 5.3).
2
+
3
+ A candidate is a *proposal* for an experiment: what to poke (target, fault
4
+ kinds, params), where (execution context), why (expected effect), and how
5
+ risky (risk band). It is not executable until it has passed the three gates:
6
+
7
+ Safety → Feasibility → Resource-conflict
8
+
9
+ A candidate that fails a gate carries the failing gate and a reason; it is
10
+ rejected, never executed.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ from dataclasses import dataclass, field
17
+ from enum import StrEnum
18
+ from typing import Any
19
+
20
+ from mayhem.domain.risks import RiskLevel
21
+
22
+
23
+ class CandidateGate(StrEnum):
24
+ """The three gates every executable candidate must pass (ADR-M5-2)."""
25
+
26
+ SAFETY = "safety" # ADR-M5 / controller safety checks
27
+ FEASIBILITY = "feasibility" # M3 runtime-capability verdicts
28
+ RESOURCE_CONFLICT = "resource_conflict" # M2 resource-conflict checks
29
+
30
+
31
+ class CandidateStatus(StrEnum):
32
+ ACCEPTED = "accepted"
33
+ REJECTED = "rejected"
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class ExperimentCandidate:
38
+ """A proposal to run one experiment against one target cell."""
39
+
40
+ target: str
41
+ fault_kinds: tuple[str, ...]
42
+ params: dict[str, Any] = field(default_factory=dict)
43
+ execution_context: str = "container"
44
+ expected_effect: str = ""
45
+ risk_band: RiskLevel = RiskLevel.LOW
46
+ seed_hint: int = 0 # ties a candidate to a deterministic generator seed
47
+
48
+ @property
49
+ def id(self) -> str:
50
+ """Stable id derived from the candidate's defining fields + seed."""
51
+ digest = hashlib.sha256(
52
+ f"{self.target}\x1f"
53
+ f"{','.join(self.fault_kinds)}\x1f"
54
+ f"{self.execution_context}\x1f"
55
+ f"{self.risk_band.value}\x1f"
56
+ f"{self.seed_hint}".encode()
57
+ ).hexdigest()[:16]
58
+ return f"cand-{digest}"
59
+
60
+ @property
61
+ def primary_fault(self) -> str:
62
+ return self.fault_kinds[0] if self.fault_kinds else ""
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class CandidateDecision:
67
+ """Outcome of routing a candidate through the three gates."""
68
+
69
+ candidate: ExperimentCandidate
70
+ status: CandidateStatus
71
+ gate: CandidateGate | None = None # which gate rejected it
72
+ reason: str = ""
73
+
74
+ @property
75
+ def accepted(self) -> bool:
76
+ return self.status is CandidateStatus.ACCEPTED
77
+
78
+ @property
79
+ def rejected(self) -> bool:
80
+ return self.status is CandidateStatus.REJECTED
81
+
82
+ @property
83
+ def summary(self) -> str:
84
+ if self.accepted:
85
+ fault = self.candidate.primary_fault
86
+ return f"ACCEPT {self.candidate.id} ({self.candidate.target}/{fault})"
87
+ gate = self.gate.value if self.gate is not None else "unknown"
88
+ return f"REJECT[{gate}] {self.candidate.id}: {self.reason}"
89
+
90
+
91
+ def accept(candidate: ExperimentCandidate) -> CandidateDecision:
92
+ return CandidateDecision(candidate=candidate, status=CandidateStatus.ACCEPTED)
93
+
94
+
95
+ def reject(candidate: ExperimentCandidate, gate: CandidateGate, reason: str) -> CandidateDecision:
96
+ return CandidateDecision(
97
+ candidate=candidate,
98
+ status=CandidateStatus.REJECTED,
99
+ gate=gate,
100
+ reason=reason,
101
+ )
@@ -0,0 +1,86 @@
1
+ """Capability and tool-manifest types (ADR-0004).
2
+
3
+ Agents declare *capabilities*; tools are wrapped behind manifests so faults ask
4
+ for outcomes, not binaries. Fallback groups let the executor pick any tool that
5
+ can deliver a capability on the current host.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+ from typing import Annotated
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
14
+
15
+ Identifier = Annotated[str, StringConstraints(pattern=r"^[a-z][a-z0-9_.-]{1,63}$")]
16
+ """Lowercase dotted identifier, e.g. ``net.latency`` or ``tc-netem``."""
17
+
18
+
19
+ class PrivilegeMode(StrEnum):
20
+ """How privileged operations are obtained on a host (ADR-0003)."""
21
+
22
+ ROOT_VIA_SYSTEMD = "root_via_systemd"
23
+ SUDO_PINNED = "sudo_pinned"
24
+ UNPRIVILEGED = "unprivileged"
25
+
26
+
27
+ class Capability(StrEnum):
28
+ """Named host abilities agents advertise during handshake."""
29
+
30
+ NET_ADMIN = "net_admin"
31
+ SYS_ADMIN = "sys_admin"
32
+ CGROUP_CONTROL = "cgroup_control"
33
+ PROCESS_CONTROL = "process_control"
34
+ FS_CONTROL = "fs_control"
35
+ DOCKER_ENGINE = "docker_engine"
36
+ PODMAN_ENGINE = "podman_engine"
37
+ KUBERNETES_ENGINE = "kubernetes_engine"
38
+
39
+
40
+ class ToolManifest(BaseModel):
41
+ """Declarative wrapper around one native or external tool.
42
+
43
+ In-tree YAML manifests deserialize into this model; adapters implement the
44
+ behavior. ``fallback_group`` ties interchangeable tools together (ADR-0010).
45
+ """
46
+
47
+ model_config = ConfigDict(frozen=True)
48
+
49
+ id: Identifier
50
+ display_name: str
51
+ binary: str
52
+ capability_group: Identifier # e.g. "net.emulation", "load.http"
53
+ provides_capabilities: frozenset[Capability] = Field(default_factory=frozenset)
54
+ requires_privilege_mode: PrivilegeMode | None = None
55
+ version_flag: tuple[str, ...] = ("--version",)
56
+ default_timeout_s: float = 30.0
57
+ output_limit_bytes: int = 1_048_576 # truncation is always explicit
58
+
59
+
60
+ class ToolAvailability(BaseModel):
61
+ """Result of probing one manifest on one host."""
62
+
63
+ model_config = ConfigDict(frozen=True)
64
+
65
+ manifest_id: Identifier
66
+ available: bool
67
+ version: str | None = None
68
+ reason_if_unavailable: str | None = None
69
+
70
+
71
+ class CapabilityReport(BaseModel):
72
+ """What an agent can actually do right now (handshake payload)."""
73
+
74
+ model_config = ConfigDict(frozen=True)
75
+
76
+ agent_id: str
77
+ host: str
78
+ privilege_mode: PrivilegeMode
79
+ capabilities: frozenset[Capability] = Field(default_factory=frozenset)
80
+ tools: tuple[ToolAvailability, ...] = ()
81
+
82
+ def has_capability(self, cap: Capability) -> bool:
83
+ return cap in self.capabilities
84
+
85
+ def missing(self, needed: frozenset[Capability]) -> frozenset[Capability]:
86
+ return needed - self.capabilities