darm-guard 0.1.0__tar.gz

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.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: darm-guard
3
+ Version: 0.1.0
4
+ Summary: Formally verified agent tool-authorization governance
5
+ Author: Olusanya Gbolahan V
6
+ License: MIT
7
+ Project-URL: Formal Backing, https://github.com/Goblohan/darm-monitor
8
+ Project-URL: Source, https://github.com/Goblohan/Darm-Guard
9
+ Keywords: ai-safety,agent-governance,formal-verification
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+
13
+ # DARM Guard
14
+
15
+ **See what your agent is doing. Then govern it.**
16
+
17
+ Formally verified agent tool-authorization governance, backed by 1,001 Lean 4 theorems.
18
+
19
+ *PerceptraAI Lab*
20
+
21
+ ## Install
22
+
23
+ From source:
24
+
25
+ git clone https://github.com/Goblohan/Darm-Guard.git
26
+ cd Darm-Guard
27
+ pip install -e .
28
+
29
+ ## Quick Start
30
+
31
+ from darm_guard import DARMGuard, Policy, Credential
32
+
33
+ guard = DARMGuard(
34
+ policy=Policy(authorized_tools=frozenset(["file_read", "web_search"])),
35
+ credential=Credential(tools=frozenset(["file_read"])),
36
+ )
37
+
38
+ result = guard.check({"file_read"}) # admitted, within credential
39
+ result = guard.check({"code_exec"}) # admitted (OBSERVE), but alerts drift
40
+
41
+ ## Three Modes
42
+
43
+ **OBSERVE (default)** -- Agent runs normally. Every tool call logged. Drift detected and alerted. Nothing blocked.
44
+
45
+ **GOVERN** -- Unauthorized tools blocked with typed ODATS diagnosis.
46
+
47
+ **ENFORCE** -- Same as GOVERN plus signed audit trail to log file.
48
+
49
+ from darm_guard import Mode
50
+
51
+ guard = DARMGuard(policy=p, credential=c, mode=Mode.GOVERN)
52
+ result = guard.check({"code_exec"})
53
+ # admitted=False
54
+ # Rejected:
55
+ # O-failure: code_exec -- not in observation model
56
+
57
+ ## ODATS Diagnosis
58
+
59
+ Every rejection identifies WHICH condition failed:
60
+
61
+ | Code | Condition | Meaning |
62
+ |------|-----------|---------|
63
+ | **O** | Observation | Tool not in the observation model |
64
+ | **D** | Domain Completeness | Dependency outside represented domain |
65
+ | **A** | Authority | Tool known but not authorized |
66
+ | **T** | Temporal Freshness | Credential expired |
67
+ | **S** | Semantic Boundary | Resolution mismatch |
68
+
69
+ Each condition is backed by a deletion-minimality witness proving it independently necessary.
70
+
71
+ ## Session Scope Tracking
72
+
73
+ Tracks cumulative scope across a session, not just per-call. Individually authorized calls can compose into unauthorized workflows.
74
+
75
+ guard.check({"file_read"}) # within credential
76
+ guard.check({"web_search"}) # outside credential, authorized by policy
77
+ print(guard.scope()) # shows cumulative drift
78
+
79
+ ## Temporal Freshness
80
+
81
+ Credentials can expire. DARM Guard detects stale credentials automatically.
82
+
83
+ from datetime import datetime, timedelta
84
+
85
+ cred = Credential(
86
+ tools=frozenset(["file_read"]),
87
+ issued_at=datetime(2026, 9, 1),
88
+ ttl=timedelta(hours=4),
89
+ )
90
+ guard = DARMGuard(policy=p, credential=cred, mode=Mode.GOVERN)
91
+
92
+ guard.check({"file_read"}, now=datetime(2026, 9, 1, 2)) # admitted (within TTL)
93
+ guard.check({"file_read"}, now=datetime(2026, 9, 1, 5)) # rejected (T-failure)
94
+
95
+ ## Credential Escalation
96
+
97
+ guard.update_credential({"code_exec"}) # human-approved expansion
98
+
99
+ ## Audit Trail
100
+
101
+ import json
102
+ for entry in guard.audit():
103
+ print(json.dumps(entry))
104
+
105
+ ## LangChain Integration
106
+
107
+ from darm_guard.integrations import guard_tools
108
+ guarded = guard_tools(agent.tools, guard=my_guard)
109
+
110
+ ## Formal Backing
111
+
112
+ Every check maps to a Lean 4 theorem in [darm-monitor](https://github.com/Goblohan/darm-monitor):
113
+
114
+ | Theorem | What it proves | File |
115
+ |---------|---------------|------|
116
+ | target_assured_of_source_and_delta | Conservation: Source + Delta -> Target | R5AssuranceConservation |
117
+ | ag_is_satisfied + causal_safety_fails | O is necessary | E14AGContractComparison |
118
+ | unsupported_authority_substitution | A is necessary | R4bAuthoritySubstitution |
119
+ | temporal_freshness_independently_necessary | T is necessary | E16TemporalFreshness |
120
+ | proposal_authority_separation | Proposal != Authority | E17ProposalAuthoritySeparation |
121
+ | check_true_implies_obligation | Bool check == Prop obligation | E21ExecutableObligationBridge |
122
+
123
+ 167 modules. 31,553 lines. 1,001 theorems. Zero sorry.
124
+
125
+ ## What Makes DARM Guard Different
126
+
127
+ **Formally verified rejection logic.** Not tested -- proved.
128
+
129
+ **Session scope tracking.** Cumulative drift, not per-call.
130
+
131
+ **ODATS diagnostic vocabulary.** Typed diagnoses, not "permission denied."
132
+
133
+ **Three-mode progressive adoption.** Visibility first, governance when ready.
134
+
135
+ **Intent-agnostic.** Same structural rejection for benign and malicious agents.
136
+
137
+ ## License
138
+
139
+ MIT
140
+
141
+ ---
142
+
143
+ **Olusanya Gbolahan V** -- **PerceptraAI Lab**
144
+
145
+ [darm-monitor](https://github.com/Goblohan/darm-monitor) | [Darm-Guard](https://github.com/Goblohan/Darm-Guard)
@@ -0,0 +1,133 @@
1
+ # DARM Guard
2
+
3
+ **See what your agent is doing. Then govern it.**
4
+
5
+ Formally verified agent tool-authorization governance, backed by 1,001 Lean 4 theorems.
6
+
7
+ *PerceptraAI Lab*
8
+
9
+ ## Install
10
+
11
+ From source:
12
+
13
+ git clone https://github.com/Goblohan/Darm-Guard.git
14
+ cd Darm-Guard
15
+ pip install -e .
16
+
17
+ ## Quick Start
18
+
19
+ from darm_guard import DARMGuard, Policy, Credential
20
+
21
+ guard = DARMGuard(
22
+ policy=Policy(authorized_tools=frozenset(["file_read", "web_search"])),
23
+ credential=Credential(tools=frozenset(["file_read"])),
24
+ )
25
+
26
+ result = guard.check({"file_read"}) # admitted, within credential
27
+ result = guard.check({"code_exec"}) # admitted (OBSERVE), but alerts drift
28
+
29
+ ## Three Modes
30
+
31
+ **OBSERVE (default)** -- Agent runs normally. Every tool call logged. Drift detected and alerted. Nothing blocked.
32
+
33
+ **GOVERN** -- Unauthorized tools blocked with typed ODATS diagnosis.
34
+
35
+ **ENFORCE** -- Same as GOVERN plus signed audit trail to log file.
36
+
37
+ from darm_guard import Mode
38
+
39
+ guard = DARMGuard(policy=p, credential=c, mode=Mode.GOVERN)
40
+ result = guard.check({"code_exec"})
41
+ # admitted=False
42
+ # Rejected:
43
+ # O-failure: code_exec -- not in observation model
44
+
45
+ ## ODATS Diagnosis
46
+
47
+ Every rejection identifies WHICH condition failed:
48
+
49
+ | Code | Condition | Meaning |
50
+ |------|-----------|---------|
51
+ | **O** | Observation | Tool not in the observation model |
52
+ | **D** | Domain Completeness | Dependency outside represented domain |
53
+ | **A** | Authority | Tool known but not authorized |
54
+ | **T** | Temporal Freshness | Credential expired |
55
+ | **S** | Semantic Boundary | Resolution mismatch |
56
+
57
+ Each condition is backed by a deletion-minimality witness proving it independently necessary.
58
+
59
+ ## Session Scope Tracking
60
+
61
+ Tracks cumulative scope across a session, not just per-call. Individually authorized calls can compose into unauthorized workflows.
62
+
63
+ guard.check({"file_read"}) # within credential
64
+ guard.check({"web_search"}) # outside credential, authorized by policy
65
+ print(guard.scope()) # shows cumulative drift
66
+
67
+ ## Temporal Freshness
68
+
69
+ Credentials can expire. DARM Guard detects stale credentials automatically.
70
+
71
+ from datetime import datetime, timedelta
72
+
73
+ cred = Credential(
74
+ tools=frozenset(["file_read"]),
75
+ issued_at=datetime(2026, 9, 1),
76
+ ttl=timedelta(hours=4),
77
+ )
78
+ guard = DARMGuard(policy=p, credential=cred, mode=Mode.GOVERN)
79
+
80
+ guard.check({"file_read"}, now=datetime(2026, 9, 1, 2)) # admitted (within TTL)
81
+ guard.check({"file_read"}, now=datetime(2026, 9, 1, 5)) # rejected (T-failure)
82
+
83
+ ## Credential Escalation
84
+
85
+ guard.update_credential({"code_exec"}) # human-approved expansion
86
+
87
+ ## Audit Trail
88
+
89
+ import json
90
+ for entry in guard.audit():
91
+ print(json.dumps(entry))
92
+
93
+ ## LangChain Integration
94
+
95
+ from darm_guard.integrations import guard_tools
96
+ guarded = guard_tools(agent.tools, guard=my_guard)
97
+
98
+ ## Formal Backing
99
+
100
+ Every check maps to a Lean 4 theorem in [darm-monitor](https://github.com/Goblohan/darm-monitor):
101
+
102
+ | Theorem | What it proves | File |
103
+ |---------|---------------|------|
104
+ | target_assured_of_source_and_delta | Conservation: Source + Delta -> Target | R5AssuranceConservation |
105
+ | ag_is_satisfied + causal_safety_fails | O is necessary | E14AGContractComparison |
106
+ | unsupported_authority_substitution | A is necessary | R4bAuthoritySubstitution |
107
+ | temporal_freshness_independently_necessary | T is necessary | E16TemporalFreshness |
108
+ | proposal_authority_separation | Proposal != Authority | E17ProposalAuthoritySeparation |
109
+ | check_true_implies_obligation | Bool check == Prop obligation | E21ExecutableObligationBridge |
110
+
111
+ 167 modules. 31,553 lines. 1,001 theorems. Zero sorry.
112
+
113
+ ## What Makes DARM Guard Different
114
+
115
+ **Formally verified rejection logic.** Not tested -- proved.
116
+
117
+ **Session scope tracking.** Cumulative drift, not per-call.
118
+
119
+ **ODATS diagnostic vocabulary.** Typed diagnoses, not "permission denied."
120
+
121
+ **Three-mode progressive adoption.** Visibility first, governance when ready.
122
+
123
+ **Intent-agnostic.** Same structural rejection for benign and malicious agents.
124
+
125
+ ## License
126
+
127
+ MIT
128
+
129
+ ---
130
+
131
+ **Olusanya Gbolahan V** -- **PerceptraAI Lab**
132
+
133
+ [darm-monitor](https://github.com/Goblohan/darm-monitor) | [Darm-Guard](https://github.com/Goblohan/Darm-Guard)
@@ -0,0 +1,13 @@
1
+ """DARM Guard - Formally verified agent tool-authorization governance."""
2
+
3
+ from .types import FailureKind, FailureWitness, DriftLevel, Credential, Policy, TransferResult
4
+ from .guard import DARMGuard, Mode
5
+ from .session import Session, SessionEvent
6
+ from .checker import check_transfer
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = [
10
+ "DARMGuard", "Mode", "Policy", "Credential", "TransferResult",
11
+ "FailureKind", "FailureWitness", "DriftLevel",
12
+ "Session", "SessionEvent", "check_transfer",
13
+ ]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+ from typing import FrozenSet, Optional, List
3
+ from datetime import datetime
4
+ from .types import Credential, Policy, FailureKind, FailureWitness, TransferResult, DriftLevel
5
+
6
+
7
+ def check_transfer(
8
+ credential: Credential,
9
+ requested_tools: FrozenSet[str],
10
+ policy: Policy,
11
+ now: Optional[datetime] = None,
12
+ ) -> TransferResult:
13
+ now = now or datetime.utcnow()
14
+ delta = requested_tools - credential.tools
15
+ failures: List[FailureWitness] = []
16
+
17
+ if credential.is_expired(now):
18
+ failures.append(FailureWitness(
19
+ kind=FailureKind.TEMPORAL_FRESHNESS,
20
+ tool="*",
21
+ detail=f"credential expired (issued {credential.issued_at}, ttl {credential.ttl})",
22
+ ))
23
+
24
+ unknown_tools = requested_tools - policy.authorized_tools - credential.tools
25
+ for tool in sorted(unknown_tools):
26
+ failures.append(FailureWitness(
27
+ kind=FailureKind.OBSERVATION,
28
+ tool=tool,
29
+ detail=f"not in observation model (unknown to policy at boundary '{policy.boundary}')",
30
+ ))
31
+
32
+ for tool in sorted(delta):
33
+ if tool in unknown_tools:
34
+ continue
35
+ if tool not in policy.authorized_tools:
36
+ failures.append(FailureWitness(
37
+ kind=FailureKind.AUTHORITY,
38
+ tool=tool,
39
+ detail=f"not authorized at boundary '{policy.boundary}'",
40
+ ))
41
+
42
+ if not delta:
43
+ drift = DriftLevel.WITHIN
44
+ elif not failures:
45
+ drift = DriftLevel.DRIFT
46
+ else:
47
+ drift = DriftLevel.VIOLATION
48
+
49
+ return TransferResult(
50
+ admitted=len(failures) == 0,
51
+ drift=drift,
52
+ delta=delta,
53
+ failures=failures,
54
+ timestamp=now,
55
+ )
@@ -0,0 +1,57 @@
1
+ import http.server
2
+ import json
3
+ import os
4
+ import threading
5
+ import webbrowser
6
+ from typing import Optional
7
+ from .guard import DARMGuard
8
+
9
+
10
+ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
11
+ guard: Optional[DARMGuard] = None
12
+ dashboard_dir: str = ""
13
+
14
+ def do_GET(self):
15
+ if self.path == "/api/state":
16
+ self.send_response(200)
17
+ self.send_header("Content-Type", "application/json")
18
+ self.send_header("Access-Control-Allow-Origin", "*")
19
+ self.end_headers()
20
+ state = {
21
+ "mode": self.guard.mode.name if self.guard else "UNKNOWN",
22
+ "scope": self.guard.scope() if self.guard else {},
23
+ "audit": self.guard.audit() if self.guard else [],
24
+ "policy_tools": sorted(self.guard.policy.authorized_tools) if self.guard else [],
25
+ }
26
+ self.wfile.write(json.dumps(state).encode())
27
+ return
28
+ if self.path == "/" or self.path == "/index.html":
29
+ self.path = "/index.html"
30
+ self.directory = self.dashboard_dir
31
+ super().do_GET()
32
+
33
+ def log_message(self, format, *args):
34
+ pass
35
+
36
+
37
+ def serve_dashboard(guard: DARMGuard, port: int = 7832, open_browser: bool = True):
38
+ dashboard_dir = os.path.join(os.path.dirname(__file__), "..", "dashboard")
39
+ dashboard_dir = os.path.abspath(dashboard_dir)
40
+ DashboardHandler.guard = guard
41
+ DashboardHandler.dashboard_dir = dashboard_dir
42
+ server = http.server.HTTPServer(("127.0.0.1", port), DashboardHandler)
43
+ print(f"DARM Guard dashboard: http://127.0.0.1:{port}")
44
+ if open_browser:
45
+ webbrowser.open(f"http://127.0.0.1:{port}")
46
+ try:
47
+ server.serve_forever()
48
+ except KeyboardInterrupt:
49
+ print("\nDashboard stopped.")
50
+ server.server_close()
51
+
52
+
53
+ def serve_dashboard_background(guard: DARMGuard, port: int = 7832):
54
+ t = threading.Thread(target=serve_dashboard, args=(guard, port, False), daemon=True)
55
+ t.start()
56
+ print(f"DARM Guard dashboard (background): http://127.0.0.1:{port}")
57
+ return t
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+ from enum import Enum, auto
3
+ from typing import FrozenSet, Optional, Set
4
+ from datetime import datetime
5
+ import json
6
+ import sys
7
+ from .types import Credential, Policy, TransferResult, DriftLevel
8
+ from .session import Session
9
+
10
+
11
+ class Mode(Enum):
12
+ OBSERVE = auto()
13
+ GOVERN = auto()
14
+ ENFORCE = auto()
15
+
16
+
17
+ class DARMGuard:
18
+ def __init__(self, policy: Policy, credential: Credential,
19
+ mode: Mode = Mode.OBSERVE, session_id: str = "",
20
+ log_file: Optional[str] = None):
21
+ self.policy = policy
22
+ self.credential = credential
23
+ self.mode = mode
24
+ self.session = Session(credential, policy, session_id)
25
+ self._log_file = log_file
26
+ self._stderr_alerts = True
27
+
28
+ def check(self, requested_tools: Set[str], now: Optional[datetime] = None) -> TransferResult:
29
+ tools = frozenset(requested_tools)
30
+ result = self.session.check(tools, now)
31
+
32
+ if self.mode == Mode.OBSERVE:
33
+ observed_result = TransferResult(
34
+ admitted=True, drift=result.drift,
35
+ delta=result.delta, failures=result.failures, timestamp=result.timestamp,
36
+ )
37
+ self._log_event(result)
38
+ self._alert(result)
39
+ return observed_result
40
+
41
+ self._log_event(result)
42
+ self._alert(result)
43
+ return result
44
+
45
+ def update_credential(self, new_tools: Set[str]) -> None:
46
+ expanded = self.credential.tools | frozenset(new_tools)
47
+ self.credential = Credential(
48
+ tools=expanded, actor=self.credential.actor,
49
+ boundary=self.credential.boundary,
50
+ issued_at=self.credential.issued_at, ttl=self.credential.ttl,
51
+ )
52
+ self.session.credential = self.credential
53
+
54
+ def scope(self) -> dict:
55
+ return self.session.scope_summary()
56
+
57
+ def audit(self) -> list:
58
+ return self.session.audit_trail()
59
+
60
+ def _log_event(self, result: TransferResult) -> None:
61
+ if self._log_file is None:
62
+ return
63
+ event = self.session.log[-1] if self.session.log else None
64
+ if event is None:
65
+ return
66
+ entry = {
67
+ "timestamp": event.timestamp.isoformat(),
68
+ "mode": self.mode.name,
69
+ "requested": sorted(event.requested_tools),
70
+ "admitted": result.admitted,
71
+ "drift": result.drift.name,
72
+ "failures": [
73
+ {"kind": f.kind.name, "tool": f.tool, "detail": f.detail}
74
+ for f in result.failures
75
+ ],
76
+ }
77
+ try:
78
+ with open(self._log_file, "a") as f:
79
+ f.write(json.dumps(entry) + "\n")
80
+ except OSError:
81
+ pass
82
+
83
+ def _alert(self, result: TransferResult) -> None:
84
+ if not self._stderr_alerts:
85
+ return
86
+ if result.drift == DriftLevel.WITHIN:
87
+ return
88
+ if result.drift == DriftLevel.DRIFT:
89
+ print(f"[DARM DRIFT] {', '.join(sorted(result.delta))} outside credential scope", file=sys.stderr)
90
+ elif result.drift == DriftLevel.VIOLATION:
91
+ for f in result.failures:
92
+ print(f"[DARM REJECT] {f.explain()}", file=sys.stderr)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+ from typing import Any, List
3
+ from ..guard import DARMGuard
4
+
5
+
6
+ def guard_tools(tools: List[Any], guard: DARMGuard) -> List[Any]:
7
+ wrapped = []
8
+ for tool in tools:
9
+ wrapped.append(_GuardedTool(tool, guard))
10
+ return wrapped
11
+
12
+
13
+ class _GuardedTool:
14
+ def __init__(self, tool: Any, guard: DARMGuard):
15
+ self._tool = tool
16
+ self._guard = guard
17
+ self.name = getattr(tool, "name", str(tool))
18
+ self.description = getattr(tool, "description", "")
19
+
20
+ def run(self, *args: Any, **kwargs: Any) -> Any:
21
+ result = self._guard.check({self.name})
22
+ if not result:
23
+ raise PermissionError(result.explain())
24
+ return self._tool.run(*args, **kwargs)
25
+
26
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
27
+ return self.run(*args, **kwargs)
28
+
29
+ def __getattr__(self, name: str) -> Any:
30
+ return getattr(self._tool, name)
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import FrozenSet, List, Optional, Callable
4
+ from datetime import datetime
5
+ from .types import Credential, Policy, TransferResult, DriftLevel
6
+ from .checker import check_transfer
7
+
8
+
9
+ @dataclass
10
+ class SessionEvent:
11
+ timestamp: datetime
12
+ requested_tools: FrozenSet[str]
13
+ cumulative_scope: FrozenSet[str]
14
+ result: TransferResult
15
+
16
+
17
+ class Session:
18
+ def __init__(self, credential: Credential, policy: Policy, session_id: str = ""):
19
+ self.credential = credential
20
+ self.policy = policy
21
+ self.session_id = session_id
22
+ self.cumulative_scope: FrozenSet[str] = frozenset()
23
+ self.log: List[SessionEvent] = []
24
+ self._on_drift: Optional[Callable[[SessionEvent], None]] = None
25
+ self._on_violation: Optional[Callable[[SessionEvent], None]] = None
26
+
27
+ def on_drift(self, callback: Callable[[SessionEvent], None]) -> None:
28
+ self._on_drift = callback
29
+
30
+ def on_violation(self, callback: Callable[[SessionEvent], None]) -> None:
31
+ self._on_violation = callback
32
+
33
+ def check(self, requested_tools: FrozenSet[str], now: Optional[datetime] = None) -> TransferResult:
34
+ now = now or datetime.utcnow()
35
+ result = check_transfer(self.credential, requested_tools, self.policy, now)
36
+ new_scope = self.cumulative_scope | requested_tools
37
+ self.cumulative_scope = new_scope
38
+
39
+ cumulative_delta = self.cumulative_scope - self.credential.tools
40
+ if cumulative_delta and result.drift == DriftLevel.WITHIN:
41
+ result = TransferResult(
42
+ admitted=result.admitted, drift=DriftLevel.DRIFT,
43
+ delta=result.delta, failures=result.failures, timestamp=result.timestamp,
44
+ )
45
+
46
+ event = SessionEvent(
47
+ timestamp=now, requested_tools=requested_tools,
48
+ cumulative_scope=self.cumulative_scope, result=result,
49
+ )
50
+ self.log.append(event)
51
+
52
+ if result.drift == DriftLevel.DRIFT and self._on_drift:
53
+ self._on_drift(event)
54
+ if result.drift == DriftLevel.VIOLATION and self._on_violation:
55
+ self._on_violation(event)
56
+
57
+ return result
58
+
59
+ def scope_summary(self) -> dict:
60
+ delta = self.cumulative_scope - self.credential.tools
61
+ return {
62
+ "session_id": self.session_id,
63
+ "credential_scope": sorted(self.credential.tools),
64
+ "cumulative_scope": sorted(self.cumulative_scope),
65
+ "delta": sorted(delta),
66
+ "drift": bool(delta),
67
+ "call_count": len(self.log),
68
+ "violations": sum(1 for e in self.log if e.result.drift == DriftLevel.VIOLATION),
69
+ }
70
+
71
+ def audit_trail(self) -> List[dict]:
72
+ return [
73
+ {
74
+ "timestamp": e.timestamp.isoformat(),
75
+ "requested": sorted(e.requested_tools),
76
+ "cumulative": sorted(e.cumulative_scope),
77
+ "admitted": e.result.admitted,
78
+ "drift": e.result.drift.name,
79
+ "delta": sorted(e.result.delta),
80
+ "failures": [
81
+ {"kind": f.kind.name, "tool": f.tool, "detail": f.detail}
82
+ for f in e.result.failures
83
+ ],
84
+ }
85
+ for e in self.log
86
+ ]
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from enum import Enum, auto
4
+ from typing import FrozenSet, List, Optional
5
+ from datetime import datetime, timedelta
6
+
7
+
8
+ class FailureKind(Enum):
9
+ OBSERVATION = auto()
10
+ DOMAIN_COMPLETENESS = auto()
11
+ AUTHORITY = auto()
12
+ TEMPORAL_FRESHNESS = auto()
13
+ SEMANTIC_BOUNDARY = auto()
14
+
15
+
16
+ class DriftLevel(Enum):
17
+ WITHIN = auto()
18
+ DRIFT = auto()
19
+ VIOLATION = auto()
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class FailureWitness:
24
+ kind: FailureKind
25
+ tool: str
26
+ detail: str
27
+
28
+ def explain(self) -> str:
29
+ labels = {
30
+ FailureKind.OBSERVATION: "O-failure",
31
+ FailureKind.DOMAIN_COMPLETENESS: "D-failure",
32
+ FailureKind.AUTHORITY: "A-failure",
33
+ FailureKind.TEMPORAL_FRESHNESS: "T-failure",
34
+ FailureKind.SEMANTIC_BOUNDARY: "S-failure",
35
+ }
36
+ return f"{labels[self.kind]}: {self.tool} -- {self.detail}"
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Credential:
41
+ tools: FrozenSet[str]
42
+ actor: str = ""
43
+ boundary: str = "default"
44
+ issued_at: Optional[datetime] = None
45
+ ttl: Optional[timedelta] = None
46
+
47
+ def is_expired(self, now: Optional[datetime] = None) -> bool:
48
+ if self.issued_at is None or self.ttl is None:
49
+ return False
50
+ now = now or datetime.utcnow()
51
+ return now > self.issued_at + self.ttl
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Policy:
56
+ authorized_tools: FrozenSet[str]
57
+ boundary: str = "default"
58
+
59
+
60
+ @dataclass
61
+ class TransferResult:
62
+ admitted: bool
63
+ drift: DriftLevel
64
+ delta: FrozenSet[str] = field(default_factory=frozenset)
65
+ failures: List[FailureWitness] = field(default_factory=list)
66
+ timestamp: datetime = field(default_factory=datetime.utcnow)
67
+
68
+ def __bool__(self) -> bool:
69
+ return self.admitted
70
+
71
+ def explain(self) -> str:
72
+ if self.admitted:
73
+ if self.drift == DriftLevel.DRIFT:
74
+ return f"Admitted with drift: {', '.join(sorted(self.delta))} outside credential but authorized."
75
+ return "Admitted: all obligations discharged."
76
+ lines = ["Rejected:"]
77
+ for f in self.failures:
78
+ lines.append(f" {f.explain()}")
79
+ return "\n".join(lines)
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: darm-guard
3
+ Version: 0.1.0
4
+ Summary: Formally verified agent tool-authorization governance
5
+ Author: Olusanya Gbolahan V
6
+ License: MIT
7
+ Project-URL: Formal Backing, https://github.com/Goblohan/darm-monitor
8
+ Project-URL: Source, https://github.com/Goblohan/Darm-Guard
9
+ Keywords: ai-safety,agent-governance,formal-verification
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+
13
+ # DARM Guard
14
+
15
+ **See what your agent is doing. Then govern it.**
16
+
17
+ Formally verified agent tool-authorization governance, backed by 1,001 Lean 4 theorems.
18
+
19
+ *PerceptraAI Lab*
20
+
21
+ ## Install
22
+
23
+ From source:
24
+
25
+ git clone https://github.com/Goblohan/Darm-Guard.git
26
+ cd Darm-Guard
27
+ pip install -e .
28
+
29
+ ## Quick Start
30
+
31
+ from darm_guard import DARMGuard, Policy, Credential
32
+
33
+ guard = DARMGuard(
34
+ policy=Policy(authorized_tools=frozenset(["file_read", "web_search"])),
35
+ credential=Credential(tools=frozenset(["file_read"])),
36
+ )
37
+
38
+ result = guard.check({"file_read"}) # admitted, within credential
39
+ result = guard.check({"code_exec"}) # admitted (OBSERVE), but alerts drift
40
+
41
+ ## Three Modes
42
+
43
+ **OBSERVE (default)** -- Agent runs normally. Every tool call logged. Drift detected and alerted. Nothing blocked.
44
+
45
+ **GOVERN** -- Unauthorized tools blocked with typed ODATS diagnosis.
46
+
47
+ **ENFORCE** -- Same as GOVERN plus signed audit trail to log file.
48
+
49
+ from darm_guard import Mode
50
+
51
+ guard = DARMGuard(policy=p, credential=c, mode=Mode.GOVERN)
52
+ result = guard.check({"code_exec"})
53
+ # admitted=False
54
+ # Rejected:
55
+ # O-failure: code_exec -- not in observation model
56
+
57
+ ## ODATS Diagnosis
58
+
59
+ Every rejection identifies WHICH condition failed:
60
+
61
+ | Code | Condition | Meaning |
62
+ |------|-----------|---------|
63
+ | **O** | Observation | Tool not in the observation model |
64
+ | **D** | Domain Completeness | Dependency outside represented domain |
65
+ | **A** | Authority | Tool known but not authorized |
66
+ | **T** | Temporal Freshness | Credential expired |
67
+ | **S** | Semantic Boundary | Resolution mismatch |
68
+
69
+ Each condition is backed by a deletion-minimality witness proving it independently necessary.
70
+
71
+ ## Session Scope Tracking
72
+
73
+ Tracks cumulative scope across a session, not just per-call. Individually authorized calls can compose into unauthorized workflows.
74
+
75
+ guard.check({"file_read"}) # within credential
76
+ guard.check({"web_search"}) # outside credential, authorized by policy
77
+ print(guard.scope()) # shows cumulative drift
78
+
79
+ ## Temporal Freshness
80
+
81
+ Credentials can expire. DARM Guard detects stale credentials automatically.
82
+
83
+ from datetime import datetime, timedelta
84
+
85
+ cred = Credential(
86
+ tools=frozenset(["file_read"]),
87
+ issued_at=datetime(2026, 9, 1),
88
+ ttl=timedelta(hours=4),
89
+ )
90
+ guard = DARMGuard(policy=p, credential=cred, mode=Mode.GOVERN)
91
+
92
+ guard.check({"file_read"}, now=datetime(2026, 9, 1, 2)) # admitted (within TTL)
93
+ guard.check({"file_read"}, now=datetime(2026, 9, 1, 5)) # rejected (T-failure)
94
+
95
+ ## Credential Escalation
96
+
97
+ guard.update_credential({"code_exec"}) # human-approved expansion
98
+
99
+ ## Audit Trail
100
+
101
+ import json
102
+ for entry in guard.audit():
103
+ print(json.dumps(entry))
104
+
105
+ ## LangChain Integration
106
+
107
+ from darm_guard.integrations import guard_tools
108
+ guarded = guard_tools(agent.tools, guard=my_guard)
109
+
110
+ ## Formal Backing
111
+
112
+ Every check maps to a Lean 4 theorem in [darm-monitor](https://github.com/Goblohan/darm-monitor):
113
+
114
+ | Theorem | What it proves | File |
115
+ |---------|---------------|------|
116
+ | target_assured_of_source_and_delta | Conservation: Source + Delta -> Target | R5AssuranceConservation |
117
+ | ag_is_satisfied + causal_safety_fails | O is necessary | E14AGContractComparison |
118
+ | unsupported_authority_substitution | A is necessary | R4bAuthoritySubstitution |
119
+ | temporal_freshness_independently_necessary | T is necessary | E16TemporalFreshness |
120
+ | proposal_authority_separation | Proposal != Authority | E17ProposalAuthoritySeparation |
121
+ | check_true_implies_obligation | Bool check == Prop obligation | E21ExecutableObligationBridge |
122
+
123
+ 167 modules. 31,553 lines. 1,001 theorems. Zero sorry.
124
+
125
+ ## What Makes DARM Guard Different
126
+
127
+ **Formally verified rejection logic.** Not tested -- proved.
128
+
129
+ **Session scope tracking.** Cumulative drift, not per-call.
130
+
131
+ **ODATS diagnostic vocabulary.** Typed diagnoses, not "permission denied."
132
+
133
+ **Three-mode progressive adoption.** Visibility first, governance when ready.
134
+
135
+ **Intent-agnostic.** Same structural rejection for benign and malicious agents.
136
+
137
+ ## License
138
+
139
+ MIT
140
+
141
+ ---
142
+
143
+ **Olusanya Gbolahan V** -- **PerceptraAI Lab**
144
+
145
+ [darm-monitor](https://github.com/Goblohan/darm-monitor) | [Darm-Guard](https://github.com/Goblohan/Darm-Guard)
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ darm_guard/__init__.py
4
+ darm_guard/checker.py
5
+ darm_guard/dashboard.py
6
+ darm_guard/guard.py
7
+ darm_guard/session.py
8
+ darm_guard/types.py
9
+ darm_guard.egg-info/PKG-INFO
10
+ darm_guard.egg-info/SOURCES.txt
11
+ darm_guard.egg-info/dependency_links.txt
12
+ darm_guard.egg-info/top_level.txt
13
+ darm_guard/integrations/__init__.py
@@ -0,0 +1 @@
1
+ darm_guard
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "darm-guard"
7
+ version = "0.1.0"
8
+ description = "Formally verified agent tool-authorization governance"
9
+ authors = [{name = "Olusanya Gbolahan V"}]
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ readme = "README.md"
13
+ keywords = ["ai-safety", "agent-governance", "formal-verification"]
14
+
15
+ [project.urls]
16
+ "Formal Backing" = "https://github.com/Goblohan/darm-monitor"
17
+ "Source" = "https://github.com/Goblohan/Darm-Guard"
18
+
19
+ [tool.setuptools.packages.find]
20
+ include = ["darm_guard*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+