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.
- mayhem/agent/__init__.py +1 -0
- mayhem/agent/cli.py +36 -0
- mayhem/agents/__init__.py +1 -0
- mayhem/agents/capabilities.py +106 -0
- mayhem/agents/executors.py +430 -0
- mayhem/agents/impact.py +729 -0
- mayhem/agents/lease_client.py +141 -0
- mayhem/agents/probes.py +284 -0
- mayhem/agents/protocol.py +134 -0
- mayhem/agents/server.py +281 -0
- mayhem/agents/sinks.py +60 -0
- mayhem/agents/transports.py +134 -0
- mayhem/agents/watchdog.py +140 -0
- mayhem/cli/__init__.py +11 -0
- mayhem/cli/app.py +154 -0
- mayhem/cli/campaign.py +496 -0
- mayhem/cli/config_cmd.py +47 -0
- mayhem/cli/context.py +23 -0
- mayhem/cli/dependency.py +429 -0
- mayhem/cli/exit_codes.py +24 -0
- mayhem/cli/experiment.py +24 -0
- mayhem/cli/lifecycle.py +805 -0
- mayhem/cli/resolver.py +72 -0
- mayhem/cli/services.py +459 -0
- mayhem/cli/style.py +101 -0
- mayhem/cli/toolkit.py +41 -0
- mayhem/cli/topology.py +127 -0
- mayhem/config.py +208 -0
- mayhem/controller/__init__.py +1 -0
- mayhem/controller/compensation.py +2156 -0
- mayhem/controller/executor.py +1719 -0
- mayhem/controller/janitor.py +196 -0
- mayhem/controller/observability_collector.py +382 -0
- mayhem/controller/observations.py +102 -0
- mayhem/controller/planner.py +715 -0
- mayhem/controller/recovery.py +245 -0
- mayhem/controller/resilience_report.py +585 -0
- mayhem/controller/resource_manager.py +457 -0
- mayhem/controller/safety.py +392 -0
- mayhem/domain/__init__.py +6 -0
- mayhem/domain/campaigns.py +118 -0
- mayhem/domain/cancellation.py +110 -0
- mayhem/domain/candidates.py +101 -0
- mayhem/domain/capabilities.py +86 -0
- mayhem/domain/catalog.py +727 -0
- mayhem/domain/checks.py +173 -0
- mayhem/domain/common.py +104 -0
- mayhem/domain/coverage.py +106 -0
- mayhem/domain/decisions.py +57 -0
- mayhem/domain/errors.py +87 -0
- mayhem/domain/events.py +61 -0
- mayhem/domain/execution_context.py +120 -0
- mayhem/domain/execution_loci.py +94 -0
- mayhem/domain/experiments.py +370 -0
- mayhem/domain/faults.py +239 -0
- mayhem/domain/identity.py +200 -0
- mayhem/domain/k8s_adapter.py +132 -0
- mayhem/domain/leases.py +186 -0
- mayhem/domain/load_strategy.py +98 -0
- mayhem/domain/m5_campaign.py +120 -0
- mayhem/domain/maniac.py +93 -0
- mayhem/domain/observability.py +146 -0
- mayhem/domain/outcomes.py +92 -0
- mayhem/domain/remote_agent_interface.py +70 -0
- mayhem/domain/resources.py +245 -0
- mayhem/domain/risks.py +61 -0
- mayhem/domain/run_outcome.py +146 -0
- mayhem/domain/runtime_adapter.py +256 -0
- mayhem/domain/success.py +329 -0
- mayhem/domain/topology.py +452 -0
- mayhem/infra/__init__.py +1 -0
- mayhem/infra/campaign_engine.py +205 -0
- mayhem/infra/candidate_gates.py +124 -0
- mayhem/infra/candidate_generator.py +110 -0
- mayhem/infra/coverage_repository.py +101 -0
- mayhem/infra/lease_repository.py +129 -0
- mayhem/infra/maniac.py +103 -0
- mayhem/infra/migrations.py +596 -0
- mayhem/infra/migrator.py +149 -0
- mayhem/infra/report.py +227 -0
- mayhem/infra/store.py +200 -0
- mayhem/py.typed +0 -0
- mayhem/spec.py +52 -0
- mayhem/toolkit/__init__.py +1 -0
- mayhem/toolkit/fingerprint.py +69 -0
- mayhem/toolkit/hashing.py +32 -0
- mayhem/toolkit/manifests/docker.yaml +11 -0
- mayhem/toolkit/manifests/podman.yaml +11 -0
- mayhem/toolkit/manifests/stress-ng.yaml +11 -0
- mayhem/toolkit/manifests/tc-netem.yaml +11 -0
- mayhem/toolkit/manifests/toxiproxy.yaml +10 -0
- mayhem/toolkit/registry.py +185 -0
- mayhem/toolkit/tool_runner.py +129 -0
- mayhem/topology/__init__.py +10 -0
- mayhem/topology/providers/__init__.py +0 -0
- mayhem/topology/providers/adapter_registry.py +60 -0
- mayhem/topology/providers/base.py +31 -0
- mayhem/topology/providers/compose.py +207 -0
- mayhem/topology/providers/docker_adapter.py +277 -0
- mayhem/topology/providers/docker_runtime.py +461 -0
- mayhem/topology/providers/podman_adapter.py +328 -0
- mayhem/topology/resolve.py +196 -0
- mayhem/topology/service.py +158 -0
- mayhem_cli-0.5.1.dist-info/METADATA +555 -0
- mayhem_cli-0.5.1.dist-info/RECORD +107 -0
- mayhem_cli-0.5.1.dist-info/WHEEL +4 -0
- 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
|