agentcontainment 0.1.0__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.
- agent_containment/__init__.py +31 -0
- agent_containment/agent_tree.py +102 -0
- agent_containment/attack_harness.py +44 -0
- agent_containment/audit.py +94 -0
- agent_containment/blast_radius.py +17 -0
- agent_containment/bootstrap.py +33 -0
- agent_containment/cgroup_enforcer.py +154 -0
- agent_containment/cilium_enforcer.py +338 -0
- agent_containment/client.py +90 -0
- agent_containment/containment.py +209 -0
- agent_containment/control.py +660 -0
- agent_containment/credentials.py +41 -0
- agent_containment/daemon.py +56 -0
- agent_containment/dprovenancekit_adapter.py +113 -0
- agent_containment/egress.py +91 -0
- agent_containment/egress_enforcement.py +170 -0
- agent_containment/enforcer.py +90 -0
- agent_containment/evidence_chain.py +180 -0
- agent_containment/gateway.py +72 -0
- agent_containment/governance_event.py +51 -0
- agent_containment/incident.py +47 -0
- agent_containment/incident_evidence.py +110 -0
- agent_containment/incident_state.py +247 -0
- agent_containment/linux_supervisor.py +87 -0
- agent_containment/models.py +27 -0
- agent_containment/policy.py +71 -0
- agent_containment/process.py +28 -0
- agent_containment/provenance.py +94 -0
- agent_containment/regression.py +175 -0
- agent_containment/regression_replay.py +74 -0
- agent_containment/review.py +38 -0
- agent_containment/runtime.py +149 -0
- agent_containment/runtime_fence.py +102 -0
- agent_containment/transport.py +281 -0
- agent_containment/verification_signal.py +103 -0
- agent_containment/warden_observation.py +79 -0
- agent_containment/warden_observer.py +43 -0
- agentcontainment-0.1.0.dist-info/METADATA +364 -0
- agentcontainment-0.1.0.dist-info/RECORD +42 -0
- agentcontainment-0.1.0.dist-info/WHEEL +5 -0
- agentcontainment-0.1.0.dist-info/licenses/LICENSE +87 -0
- agentcontainment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .bootstrap import BootstrapAdmissionError, require_controller_available
|
|
2
|
+
from .control import ContainmentService, ManagedAgent, RecoveryAuthorization
|
|
3
|
+
from .cgroup_enforcer import CgroupV2Enforcer
|
|
4
|
+
from .cilium_enforcer import CiliumNetworkPolicyEnforcer
|
|
5
|
+
from .egress import EgressController, EgressLease, hard_close_socket
|
|
6
|
+
from .governance_event import GovernanceEvent
|
|
7
|
+
from .enforcer import Enforcer, EnforcementResult, EnforcementStatus, NoopEnforcer
|
|
8
|
+
from .gateway import ActionGateway
|
|
9
|
+
from .incident_state import IncidentRecord, IncidentRegistry, IncidentState
|
|
10
|
+
from .linux_supervisor import LinuxCgroupSupervisor
|
|
11
|
+
from .models import Action, Decision, DecisionType
|
|
12
|
+
from .policy import PolicyEngine
|
|
13
|
+
from .provenance import InMemoryProvenanceSink, ProvenanceEmitter, ProvenanceRecord, ProvenanceSink, provenance_record
|
|
14
|
+
from .regression import RegressionFixture, RegressionFixtureBuilder, assert_regression
|
|
15
|
+
from .regression_replay import RegressionReplayResult, assert_fixture_replays, replay_fixture
|
|
16
|
+
from .runtime_fence import FenceRecord, RuntimeFenceRegistry
|
|
17
|
+
from .warden_observation import WardenObservation
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Action", "ActionGateway", "Decision", "DecisionType",
|
|
21
|
+
"BootstrapAdmissionError", "require_controller_available",
|
|
22
|
+
"ContainmentService", "ManagedAgent", "RecoveryAuthorization",
|
|
23
|
+
"IncidentRecord", "IncidentRegistry", "IncidentState", "GovernanceEvent",
|
|
24
|
+
"ProvenanceRecord", "ProvenanceSink", "ProvenanceEmitter", "InMemoryProvenanceSink", "provenance_record",
|
|
25
|
+
"RegressionFixture", "RegressionFixtureBuilder", "assert_regression",
|
|
26
|
+
"RegressionReplayResult", "replay_fixture", "assert_fixture_replays",
|
|
27
|
+
"EgressController", "EgressLease", "PolicyEngine", "hard_close_socket",
|
|
28
|
+
"LinuxCgroupSupervisor", "FenceRecord", "RuntimeFenceRegistry",
|
|
29
|
+
"Enforcer", "EnforcementResult", "EnforcementStatus", "NoopEnforcer",
|
|
30
|
+
"CgroupV2Enforcer", "CiliumNetworkPolicyEnforcer", "WardenObservation",
|
|
31
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from threading import RLock
|
|
3
|
+
|
|
4
|
+
from .containment import CapabilitySet, ContainmentController
|
|
5
|
+
from .runtime import Runtime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AgentNode:
|
|
10
|
+
agent_id: str
|
|
11
|
+
parent_id: str | None
|
|
12
|
+
runtime: Runtime
|
|
13
|
+
containment: ContainmentController
|
|
14
|
+
children: set[str] = field(default_factory=set)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentTree:
|
|
18
|
+
"""Tracks parent/child relationships with controller-owned spawn/containment serialization."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.nodes: dict[str, AgentNode] = {}
|
|
22
|
+
self._lock = RLock()
|
|
23
|
+
|
|
24
|
+
def register_root(
|
|
25
|
+
self,
|
|
26
|
+
agent_id: str,
|
|
27
|
+
capabilities: set[str] | None = None,
|
|
28
|
+
) -> AgentNode:
|
|
29
|
+
with self._lock:
|
|
30
|
+
if agent_id in self.nodes:
|
|
31
|
+
raise ValueError(f"agent already registered: {agent_id}")
|
|
32
|
+
runtime = Runtime(agent_id)
|
|
33
|
+
controller = ContainmentController(
|
|
34
|
+
runtime,
|
|
35
|
+
CapabilitySet(set(capabilities or set())),
|
|
36
|
+
)
|
|
37
|
+
node = AgentNode(agent_id, None, runtime, controller)
|
|
38
|
+
self.nodes[agent_id] = node
|
|
39
|
+
return node
|
|
40
|
+
|
|
41
|
+
def spawn(
|
|
42
|
+
self,
|
|
43
|
+
parent_id: str,
|
|
44
|
+
child_id: str,
|
|
45
|
+
capabilities: set[str] | None = None,
|
|
46
|
+
) -> AgentNode:
|
|
47
|
+
with self._lock:
|
|
48
|
+
if child_id in self.nodes:
|
|
49
|
+
raise ValueError(f"agent already registered: {child_id}")
|
|
50
|
+
parent = self._require(parent_id)
|
|
51
|
+
|
|
52
|
+
if not parent.runtime.can_execute or parent.containment.contained:
|
|
53
|
+
raise RuntimeError(f"parent agent cannot spawn children: {parent_id}")
|
|
54
|
+
|
|
55
|
+
inherited = set(parent.containment.capabilities.capabilities)
|
|
56
|
+
if capabilities is not None:
|
|
57
|
+
inherited &= set(capabilities)
|
|
58
|
+
|
|
59
|
+
runtime = Runtime(child_id)
|
|
60
|
+
controller = ContainmentController(
|
|
61
|
+
runtime,
|
|
62
|
+
CapabilitySet(inherited),
|
|
63
|
+
)
|
|
64
|
+
node = AgentNode(child_id, parent_id, runtime, controller)
|
|
65
|
+
self.nodes[child_id] = node
|
|
66
|
+
parent.children.add(child_id)
|
|
67
|
+
return node
|
|
68
|
+
|
|
69
|
+
def contain(self, agent_id: str) -> list[str]:
|
|
70
|
+
"""Contain an agent and every descendant known at the containment fence."""
|
|
71
|
+
with self._lock:
|
|
72
|
+
root = self._require(agent_id)
|
|
73
|
+
contained: list[str] = []
|
|
74
|
+
stack = [root.agent_id]
|
|
75
|
+
|
|
76
|
+
while stack:
|
|
77
|
+
current_id = stack.pop()
|
|
78
|
+
current = self._require(current_id)
|
|
79
|
+
current.containment.contain()
|
|
80
|
+
contained.append(current_id)
|
|
81
|
+
stack.extend(sorted(current.children, reverse=True))
|
|
82
|
+
|
|
83
|
+
return contained
|
|
84
|
+
|
|
85
|
+
def descendants(self, agent_id: str) -> list[str]:
|
|
86
|
+
with self._lock:
|
|
87
|
+
self._require(agent_id)
|
|
88
|
+
result: list[str] = []
|
|
89
|
+
stack = list(self.nodes[agent_id].children)
|
|
90
|
+
|
|
91
|
+
while stack:
|
|
92
|
+
child_id = stack.pop()
|
|
93
|
+
result.append(child_id)
|
|
94
|
+
stack.extend(self.nodes[child_id].children)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
def _require(self, agent_id: str) -> AgentNode:
|
|
99
|
+
try:
|
|
100
|
+
return self.nodes[agent_id]
|
|
101
|
+
except KeyError as exc:
|
|
102
|
+
raise KeyError(f"unknown agent: {agent_id}") from exc
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Controlled adversarial harness for testing containment invariants.
|
|
2
|
+
|
|
3
|
+
This module never executes real external commands, network calls, or destructive
|
|
4
|
+
operations. It models an agent attempting to bypass the control plane.
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from .models import Action, DecisionType
|
|
8
|
+
from .gateway import ActionGateway
|
|
9
|
+
from .containment import ContainmentController
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AttackAttempt:
|
|
13
|
+
name: str
|
|
14
|
+
action: Action
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AttackResult:
|
|
18
|
+
name: str
|
|
19
|
+
decision: DecisionType
|
|
20
|
+
blocked: bool
|
|
21
|
+
reason: str
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class AttackHarness:
|
|
25
|
+
gateway: ActionGateway
|
|
26
|
+
containment: ContainmentController
|
|
27
|
+
results: list[AttackResult] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def attempt(self, attack: AttackAttempt) -> AttackResult:
|
|
30
|
+
decision = self.gateway.authorize(attack.action)
|
|
31
|
+
result = AttackResult(
|
|
32
|
+
name=attack.name,
|
|
33
|
+
decision=decision.decision,
|
|
34
|
+
blocked=decision.decision is not DecisionType.ALLOW,
|
|
35
|
+
reason=decision.reason,
|
|
36
|
+
)
|
|
37
|
+
self.results.append(result)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
def assert_contained(self) -> None:
|
|
41
|
+
failures = [r for r in self.results if not r.blocked]
|
|
42
|
+
if failures:
|
|
43
|
+
names = ", ".join(r.name for r in failures)
|
|
44
|
+
raise AssertionError(f"containment bypasses detected: {names}")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Controller-owned tamper-evident JSONL audit chain."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from time import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
GENESIS = "0" * 64
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _canonical(value: dict[str, Any]) -> bytes:
|
|
15
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuditLog:
|
|
19
|
+
"""Append-only hash chain.
|
|
20
|
+
|
|
21
|
+
The chain is tamper-evident, not externally immutable: an attacker who can
|
|
22
|
+
delete the file can remove history. Verification therefore fails closed
|
|
23
|
+
before appending to an already-corrupt chain.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: str | Path):
|
|
27
|
+
self.path = Path(path)
|
|
28
|
+
self._lock = RLock()
|
|
29
|
+
|
|
30
|
+
def record(self, event_type: str, *, agent_id: str, action_id: str | None = None,
|
|
31
|
+
decision: str | None = None, reason: str | None = None,
|
|
32
|
+
timestamp: float | None = None, **fields: Any) -> dict[str, Any]:
|
|
33
|
+
if not isinstance(event_type, str) or not event_type:
|
|
34
|
+
raise ValueError("event_type must be non-empty")
|
|
35
|
+
if not isinstance(agent_id, str) or not agent_id:
|
|
36
|
+
raise ValueError("agent_id must be non-empty")
|
|
37
|
+
with self._lock:
|
|
38
|
+
if self.path.exists():
|
|
39
|
+
ok, verification_reason = self.verify()
|
|
40
|
+
if not ok:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
f"refusing to append to invalid audit chain: {verification_reason}"
|
|
43
|
+
)
|
|
44
|
+
lines = [line for line in self.path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
45
|
+
else:
|
|
46
|
+
lines = []
|
|
47
|
+
|
|
48
|
+
previous_hash = GENESIS
|
|
49
|
+
if lines:
|
|
50
|
+
previous_hash = json.loads(lines[-1])["hash"]
|
|
51
|
+
|
|
52
|
+
event: dict[str, Any] = {
|
|
53
|
+
"version": 1,
|
|
54
|
+
"sequence": 1 if not lines else len(lines) + 1,
|
|
55
|
+
"timestamp": time() if timestamp is None else timestamp,
|
|
56
|
+
"event_type": event_type,
|
|
57
|
+
"agent_id": agent_id,
|
|
58
|
+
"action_id": action_id,
|
|
59
|
+
"decision": decision,
|
|
60
|
+
"reason": reason,
|
|
61
|
+
"previous_hash": previous_hash,
|
|
62
|
+
**fields,
|
|
63
|
+
}
|
|
64
|
+
event["hash"] = hashlib.sha256(_canonical(event)).hexdigest()
|
|
65
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
with self.path.open("a", encoding="utf-8") as handle:
|
|
67
|
+
handle.write(json.dumps(event, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n")
|
|
68
|
+
return event
|
|
69
|
+
|
|
70
|
+
def verify(self) -> tuple[bool, str]:
|
|
71
|
+
with self._lock:
|
|
72
|
+
if not self.path.exists():
|
|
73
|
+
return True, "empty audit log"
|
|
74
|
+
expected_previous = GENESIS
|
|
75
|
+
expected_sequence = 1
|
|
76
|
+
try:
|
|
77
|
+
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
78
|
+
for line in lines:
|
|
79
|
+
if not line.strip():
|
|
80
|
+
continue
|
|
81
|
+
event = json.loads(line)
|
|
82
|
+
stored = event.pop("hash")
|
|
83
|
+
if event.get("sequence") != expected_sequence:
|
|
84
|
+
return False, f"sequence mismatch at {expected_sequence}"
|
|
85
|
+
if event.get("previous_hash") != expected_previous:
|
|
86
|
+
return False, f"previous hash mismatch at sequence {expected_sequence}"
|
|
87
|
+
actual = hashlib.sha256(_canonical(event)).hexdigest()
|
|
88
|
+
if stored != actual:
|
|
89
|
+
return False, f"hash mismatch at sequence {expected_sequence}"
|
|
90
|
+
expected_previous = stored
|
|
91
|
+
expected_sequence += 1
|
|
92
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError):
|
|
93
|
+
return False, "malformed audit log"
|
|
94
|
+
return True, "audit chain valid"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
@dataclass
|
|
4
|
+
class ResourceGraph:
|
|
5
|
+
touched: set[str] = field(default_factory=set)
|
|
6
|
+
modified: set[str] = field(default_factory=set)
|
|
7
|
+
external: set[str] = field(default_factory=set)
|
|
8
|
+
|
|
9
|
+
def record(self, resource: str, *, modified: bool = False, external: bool = False) -> None:
|
|
10
|
+
self.touched.add(resource)
|
|
11
|
+
if modified:
|
|
12
|
+
self.modified.add(resource)
|
|
13
|
+
if external:
|
|
14
|
+
self.external.add(resource)
|
|
15
|
+
|
|
16
|
+
def summary(self) -> dict[str, int]:
|
|
17
|
+
return {"touched": len(self.touched), "modified": len(self.modified), "external": len(self.external)}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Controller-owned bootstrap admission checks.
|
|
2
|
+
|
|
3
|
+
This module is a pre-admission guard for trusted launchers. It is intentionally
|
|
4
|
+
not an authorization API: the agent must not be able to choose or bypass the
|
|
5
|
+
launcher that invokes it. Production admission should be enforced by the host
|
|
6
|
+
service manager or container runtime before the agent workload is started.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import socket
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BootstrapAdmissionError(RuntimeError):
|
|
15
|
+
"""Raised when controller availability cannot be established."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def require_controller_available(
|
|
19
|
+
socket_path: str | Path, *, timeout: float = 1.0
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Require a live controller IPC endpoint before admitting a workload."""
|
|
22
|
+
path = Path(socket_path)
|
|
23
|
+
if not path.exists():
|
|
24
|
+
raise BootstrapAdmissionError("controller socket is unavailable")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
|
28
|
+
client.settimeout(timeout)
|
|
29
|
+
client.connect(str(path))
|
|
30
|
+
except OSError as exc:
|
|
31
|
+
raise BootstrapAdmissionError(
|
|
32
|
+
"controller endpoint is unavailable"
|
|
33
|
+
) from exc
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
from .enforcer import EnforcementResult, EnforcementStatus
|
|
9
|
+
from .linux_supervisor import LinuxCgroupSupervisor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class _CgroupIdentity:
|
|
14
|
+
device: int
|
|
15
|
+
inode: int
|
|
16
|
+
ctime_ns: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CgroupV2Enforcer:
|
|
20
|
+
"""Adapt a dedicated, identity-bound cgroup v2 workload boundary to Enforcer."""
|
|
21
|
+
|
|
22
|
+
name = "cgroup-v2"
|
|
23
|
+
|
|
24
|
+
def __init__(self, cgroup_paths: Mapping[str, str | os.PathLike[str]]):
|
|
25
|
+
self._cgroup_paths = {
|
|
26
|
+
agent_id: Path(path) for agent_id, path in cgroup_paths.items()
|
|
27
|
+
}
|
|
28
|
+
self._identities = {
|
|
29
|
+
agent_id: self._identity(path)
|
|
30
|
+
for agent_id, path in self._cgroup_paths.items()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _identity(path: Path) -> _CgroupIdentity:
|
|
35
|
+
stat = path.stat()
|
|
36
|
+
if not path.is_dir():
|
|
37
|
+
raise ValueError(f"cgroup path is not a directory: {path}")
|
|
38
|
+
return _CgroupIdentity(stat.st_dev, stat.st_ino, stat.st_ctime_ns)
|
|
39
|
+
|
|
40
|
+
def _path(self, agent_id: str) -> Path | None:
|
|
41
|
+
return self._cgroup_paths.get(agent_id)
|
|
42
|
+
|
|
43
|
+
def _missing(self, agent_id: str) -> EnforcementResult:
|
|
44
|
+
return EnforcementResult(
|
|
45
|
+
self.name,
|
|
46
|
+
EnforcementStatus.NOT_CONFIGURED,
|
|
47
|
+
f"no cgroup configured for agent {agent_id}",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _verify_identity(self, agent_id: str, path: Path) -> EnforcementResult | None:
|
|
51
|
+
expected = self._identities[agent_id]
|
|
52
|
+
try:
|
|
53
|
+
actual = self._identity(path)
|
|
54
|
+
except (OSError, ValueError) as exc:
|
|
55
|
+
return EnforcementResult(
|
|
56
|
+
self.name,
|
|
57
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
58
|
+
f"cgroup identity unavailable: {type(exc).__name__}: {exc}",
|
|
59
|
+
)
|
|
60
|
+
if actual != expected:
|
|
61
|
+
return EnforcementResult(
|
|
62
|
+
self.name,
|
|
63
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
64
|
+
f"cgroup identity changed for {agent_id}",
|
|
65
|
+
)
|
|
66
|
+
# A directory with the right filesystem identity is not sufficient:
|
|
67
|
+
# require the kernel cgroup v2 control files before trusting its state.
|
|
68
|
+
for control in ("cgroup.events", "cgroup.kill"):
|
|
69
|
+
if not (path / control).is_file():
|
|
70
|
+
return EnforcementResult(
|
|
71
|
+
self.name,
|
|
72
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
73
|
+
f"required cgroup control file is missing: {control}",
|
|
74
|
+
)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def contain(self, agent_id: str) -> EnforcementResult:
|
|
78
|
+
path = self._path(agent_id)
|
|
79
|
+
if path is None:
|
|
80
|
+
return self._missing(agent_id)
|
|
81
|
+
try:
|
|
82
|
+
identity_failure = self._verify_identity(agent_id, path)
|
|
83
|
+
if identity_failure:
|
|
84
|
+
return identity_failure
|
|
85
|
+
LinuxCgroupSupervisor.contain(path)
|
|
86
|
+
return EnforcementResult(self.name, EnforcementStatus.ENFORCED)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
return EnforcementResult(
|
|
89
|
+
self.name, EnforcementStatus.DEGRADED, f"{type(exc).__name__}: {exc}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def verify_contained(self, agent_id: str) -> EnforcementResult:
|
|
93
|
+
path = self._path(agent_id)
|
|
94
|
+
if path is None:
|
|
95
|
+
return self._missing(agent_id)
|
|
96
|
+
try:
|
|
97
|
+
identity_failure = self._verify_identity(agent_id, path)
|
|
98
|
+
if identity_failure:
|
|
99
|
+
return identity_failure
|
|
100
|
+
if LinuxCgroupSupervisor.is_populated(path):
|
|
101
|
+
return EnforcementResult(
|
|
102
|
+
self.name,
|
|
103
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
104
|
+
f"cgroup remains populated for {agent_id}",
|
|
105
|
+
)
|
|
106
|
+
return EnforcementResult(self.name, EnforcementStatus.ENFORCED)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
return EnforcementResult(
|
|
109
|
+
self.name,
|
|
110
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
111
|
+
f"{type(exc).__name__}: {exc}",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def release(self, agent_id: str) -> EnforcementResult:
|
|
115
|
+
path = self._path(agent_id)
|
|
116
|
+
if path is None:
|
|
117
|
+
return self._missing(agent_id)
|
|
118
|
+
try:
|
|
119
|
+
identity_failure = self._verify_identity(agent_id, path)
|
|
120
|
+
if identity_failure:
|
|
121
|
+
return identity_failure
|
|
122
|
+
if LinuxCgroupSupervisor.is_populated(path):
|
|
123
|
+
return EnforcementResult(
|
|
124
|
+
self.name,
|
|
125
|
+
EnforcementStatus.DEGRADED,
|
|
126
|
+
f"cgroup remains populated for {agent_id}",
|
|
127
|
+
)
|
|
128
|
+
return EnforcementResult(self.name, EnforcementStatus.RELEASED)
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
return EnforcementResult(
|
|
131
|
+
self.name, EnforcementStatus.DEGRADED, f"{type(exc).__name__}: {exc}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def verify_released(self, agent_id: str) -> EnforcementResult:
|
|
135
|
+
path = self._path(agent_id)
|
|
136
|
+
if path is None:
|
|
137
|
+
return self._missing(agent_id)
|
|
138
|
+
try:
|
|
139
|
+
identity_failure = self._verify_identity(agent_id, path)
|
|
140
|
+
if identity_failure:
|
|
141
|
+
return identity_failure
|
|
142
|
+
if LinuxCgroupSupervisor.is_populated(path):
|
|
143
|
+
return EnforcementResult(
|
|
144
|
+
self.name,
|
|
145
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
146
|
+
f"cgroup is unexpectedly populated for {agent_id}",
|
|
147
|
+
)
|
|
148
|
+
return EnforcementResult(self.name, EnforcementStatus.RELEASED)
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
return EnforcementResult(
|
|
151
|
+
self.name,
|
|
152
|
+
EnforcementStatus.VERIFICATION_FAILED,
|
|
153
|
+
f"{type(exc).__name__}: {exc}",
|
|
154
|
+
)
|