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.
- darm_guard-0.1.0/PKG-INFO +145 -0
- darm_guard-0.1.0/README.md +133 -0
- darm_guard-0.1.0/darm_guard/__init__.py +13 -0
- darm_guard-0.1.0/darm_guard/checker.py +55 -0
- darm_guard-0.1.0/darm_guard/dashboard.py +57 -0
- darm_guard-0.1.0/darm_guard/guard.py +92 -0
- darm_guard-0.1.0/darm_guard/integrations/__init__.py +30 -0
- darm_guard-0.1.0/darm_guard/session.py +86 -0
- darm_guard-0.1.0/darm_guard/types.py +79 -0
- darm_guard-0.1.0/darm_guard.egg-info/PKG-INFO +145 -0
- darm_guard-0.1.0/darm_guard.egg-info/SOURCES.txt +13 -0
- darm_guard-0.1.0/darm_guard.egg-info/dependency_links.txt +1 -0
- darm_guard-0.1.0/darm_guard.egg-info/top_level.txt +1 -0
- darm_guard-0.1.0/pyproject.toml +20 -0
- darm_guard-0.1.0/setup.cfg +4 -0
|
@@ -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
|
+
|
|
@@ -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*"]
|