network-ai 3.0.0
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.
- package/LICENSE +21 -0
- package/QUICKSTART.md +260 -0
- package/README.md +604 -0
- package/SKILL.md +568 -0
- package/dist/adapters/adapter-registry.d.ts +94 -0
- package/dist/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/adapters/adapter-registry.js +355 -0
- package/dist/adapters/adapter-registry.js.map +1 -0
- package/dist/adapters/agno-adapter.d.ts +112 -0
- package/dist/adapters/agno-adapter.d.ts.map +1 -0
- package/dist/adapters/agno-adapter.js +140 -0
- package/dist/adapters/agno-adapter.js.map +1 -0
- package/dist/adapters/autogen-adapter.d.ts +67 -0
- package/dist/adapters/autogen-adapter.d.ts.map +1 -0
- package/dist/adapters/autogen-adapter.js +141 -0
- package/dist/adapters/autogen-adapter.js.map +1 -0
- package/dist/adapters/base-adapter.d.ts +51 -0
- package/dist/adapters/base-adapter.d.ts.map +1 -0
- package/dist/adapters/base-adapter.js +103 -0
- package/dist/adapters/base-adapter.js.map +1 -0
- package/dist/adapters/crewai-adapter.d.ts +72 -0
- package/dist/adapters/crewai-adapter.d.ts.map +1 -0
- package/dist/adapters/crewai-adapter.js +148 -0
- package/dist/adapters/crewai-adapter.js.map +1 -0
- package/dist/adapters/custom-adapter.d.ts +74 -0
- package/dist/adapters/custom-adapter.d.ts.map +1 -0
- package/dist/adapters/custom-adapter.js +142 -0
- package/dist/adapters/custom-adapter.js.map +1 -0
- package/dist/adapters/dspy-adapter.d.ts +70 -0
- package/dist/adapters/dspy-adapter.d.ts.map +1 -0
- package/dist/adapters/dspy-adapter.js +127 -0
- package/dist/adapters/dspy-adapter.js.map +1 -0
- package/dist/adapters/haystack-adapter.d.ts +83 -0
- package/dist/adapters/haystack-adapter.d.ts.map +1 -0
- package/dist/adapters/haystack-adapter.js +149 -0
- package/dist/adapters/haystack-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +56 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/langchain-adapter.d.ts +51 -0
- package/dist/adapters/langchain-adapter.d.ts.map +1 -0
- package/dist/adapters/langchain-adapter.js +134 -0
- package/dist/adapters/langchain-adapter.js.map +1 -0
- package/dist/adapters/llamaindex-adapter.d.ts +89 -0
- package/dist/adapters/llamaindex-adapter.d.ts.map +1 -0
- package/dist/adapters/llamaindex-adapter.js +135 -0
- package/dist/adapters/llamaindex-adapter.js.map +1 -0
- package/dist/adapters/mcp-adapter.d.ts +90 -0
- package/dist/adapters/mcp-adapter.d.ts.map +1 -0
- package/dist/adapters/mcp-adapter.js +200 -0
- package/dist/adapters/mcp-adapter.js.map +1 -0
- package/dist/adapters/openai-assistants-adapter.d.ts +94 -0
- package/dist/adapters/openai-assistants-adapter.d.ts.map +1 -0
- package/dist/adapters/openai-assistants-adapter.js +130 -0
- package/dist/adapters/openai-assistants-adapter.js.map +1 -0
- package/dist/adapters/openclaw-adapter.d.ts +21 -0
- package/dist/adapters/openclaw-adapter.d.ts.map +1 -0
- package/dist/adapters/openclaw-adapter.js +140 -0
- package/dist/adapters/openclaw-adapter.js.map +1 -0
- package/dist/adapters/semantic-kernel-adapter.d.ts +73 -0
- package/dist/adapters/semantic-kernel-adapter.d.ts.map +1 -0
- package/dist/adapters/semantic-kernel-adapter.js +123 -0
- package/dist/adapters/semantic-kernel-adapter.js.map +1 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1428 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/blackboard-validator.d.ts +205 -0
- package/dist/lib/blackboard-validator.d.ts.map +1 -0
- package/dist/lib/blackboard-validator.js +756 -0
- package/dist/lib/blackboard-validator.js.map +1 -0
- package/dist/lib/locked-blackboard.d.ts +174 -0
- package/dist/lib/locked-blackboard.d.ts.map +1 -0
- package/dist/lib/locked-blackboard.js +654 -0
- package/dist/lib/locked-blackboard.js.map +1 -0
- package/dist/lib/swarm-utils.d.ts +136 -0
- package/dist/lib/swarm-utils.d.ts.map +1 -0
- package/dist/lib/swarm-utils.js +510 -0
- package/dist/lib/swarm-utils.js.map +1 -0
- package/dist/security.d.ts +269 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +713 -0
- package/dist/security.js.map +1 -0
- package/package.json +84 -0
- package/scripts/blackboard.py +819 -0
- package/scripts/check_permission.py +331 -0
- package/scripts/revoke_token.py +243 -0
- package/scripts/swarm_guard.py +1140 -0
- package/scripts/validate_token.py +97 -0
- package/types/agent-adapter.d.ts +244 -0
- package/types/openclaw-core.d.ts +52 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AuthGuardian Permission Checker
|
|
4
|
+
|
|
5
|
+
Evaluates permission requests for accessing sensitive resources
|
|
6
|
+
(DATABASE, PAYMENTS, EMAIL, FILE_EXPORT).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python check_permission.py --agent AGENT_ID --resource RESOURCE_TYPE \
|
|
10
|
+
--justification "REASON" [--scope SCOPE]
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
python check_permission.py --agent data_analyst --resource DATABASE \
|
|
14
|
+
--justification "Need customer order history for sales report" \
|
|
15
|
+
--scope "read:orders"
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import uuid
|
|
23
|
+
from datetime import datetime, timedelta, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
# Configuration
|
|
28
|
+
GRANT_TOKEN_TTL_MINUTES = 5
|
|
29
|
+
GRANTS_FILE = Path(__file__).parent.parent / "data" / "active_grants.json"
|
|
30
|
+
AUDIT_LOG = Path(__file__).parent.parent / "data" / "audit_log.jsonl"
|
|
31
|
+
|
|
32
|
+
# Default trust levels for known agents
|
|
33
|
+
DEFAULT_TRUST_LEVELS = {
|
|
34
|
+
"orchestrator": 0.9,
|
|
35
|
+
"data_analyst": 0.8,
|
|
36
|
+
"strategy_advisor": 0.7,
|
|
37
|
+
"risk_assessor": 0.85,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Base risk scores for resource types
|
|
41
|
+
BASE_RISKS = {
|
|
42
|
+
"DATABASE": 0.5, # Internal database access
|
|
43
|
+
"PAYMENTS": 0.7, # Payment/financial systems
|
|
44
|
+
"EMAIL": 0.4, # Email sending capability
|
|
45
|
+
"FILE_EXPORT": 0.6, # Exporting data to files
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Default restrictions by resource type
|
|
49
|
+
RESTRICTIONS = {
|
|
50
|
+
"DATABASE": ["read_only", "max_records:100"],
|
|
51
|
+
"PAYMENTS": ["read_only", "no_pii_fields", "audit_required"],
|
|
52
|
+
"EMAIL": ["rate_limit:10_per_minute"],
|
|
53
|
+
"FILE_EXPORT": ["anonymize_pii", "local_only"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_data_dir():
|
|
58
|
+
"""Ensure data directory exists."""
|
|
59
|
+
data_dir = Path(__file__).parent.parent / "data"
|
|
60
|
+
data_dir.mkdir(exist_ok=True)
|
|
61
|
+
return data_dir
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def score_justification(justification: str) -> float:
|
|
65
|
+
"""
|
|
66
|
+
Score the quality of a justification.
|
|
67
|
+
|
|
68
|
+
Criteria:
|
|
69
|
+
- Length (more detail = better)
|
|
70
|
+
- Contains task-related keywords
|
|
71
|
+
- Contains specificity keywords
|
|
72
|
+
- Doesn't contain test/debug keywords
|
|
73
|
+
"""
|
|
74
|
+
score = 0.0
|
|
75
|
+
|
|
76
|
+
if len(justification) > 20:
|
|
77
|
+
score += 0.2
|
|
78
|
+
if len(justification) > 50:
|
|
79
|
+
score += 0.2
|
|
80
|
+
if re.search(r'\b(task|purpose|need|require|generate|analyze|create|process)\b',
|
|
81
|
+
justification, re.IGNORECASE):
|
|
82
|
+
score += 0.2
|
|
83
|
+
if re.search(r'\b(specific|particular|exact|quarterly|annual|report|summary)\b',
|
|
84
|
+
justification, re.IGNORECASE):
|
|
85
|
+
score += 0.2
|
|
86
|
+
if not re.search(r'\b(test|debug|try|experiment)\b', justification, re.IGNORECASE):
|
|
87
|
+
score += 0.2
|
|
88
|
+
|
|
89
|
+
return min(score, 1.0)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def assess_risk(resource_type: str, scope: Optional[str] = None) -> float:
|
|
93
|
+
"""
|
|
94
|
+
Assess the risk level of a permission request.
|
|
95
|
+
|
|
96
|
+
Factors:
|
|
97
|
+
- Base risk of resource type
|
|
98
|
+
- Scope breadth (broad scopes = higher risk)
|
|
99
|
+
- Write operations (higher risk)
|
|
100
|
+
"""
|
|
101
|
+
risk = BASE_RISKS.get(resource_type, 0.5)
|
|
102
|
+
|
|
103
|
+
# Broad scopes increase risk
|
|
104
|
+
if not scope or scope in ("*", "all"):
|
|
105
|
+
risk += 0.2
|
|
106
|
+
|
|
107
|
+
# Write operations increase risk
|
|
108
|
+
if scope and re.search(r'\b(write|delete|update|modify|create)\b', scope, re.IGNORECASE):
|
|
109
|
+
risk += 0.2
|
|
110
|
+
|
|
111
|
+
return min(risk, 1.0)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def generate_grant_token() -> str:
|
|
115
|
+
"""Generate a unique grant token."""
|
|
116
|
+
return f"grant_{uuid.uuid4().hex}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def log_audit(action: str, details: dict[str, Any]) -> None:
|
|
120
|
+
"""Append entry to audit log."""
|
|
121
|
+
ensure_data_dir()
|
|
122
|
+
entry: dict[str, Any] = {
|
|
123
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
124
|
+
"action": action,
|
|
125
|
+
"details": details
|
|
126
|
+
}
|
|
127
|
+
with open(AUDIT_LOG, "a") as f:
|
|
128
|
+
f.write(json.dumps(entry) + "\n")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def save_grant(grant: dict[str, Any]) -> None:
|
|
132
|
+
"""Save grant to persistent storage."""
|
|
133
|
+
ensure_data_dir()
|
|
134
|
+
grants = {}
|
|
135
|
+
if GRANTS_FILE.exists():
|
|
136
|
+
try:
|
|
137
|
+
grants = json.loads(GRANTS_FILE.read_text())
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
grants = {}
|
|
140
|
+
|
|
141
|
+
grants[grant["token"]] = grant
|
|
142
|
+
GRANTS_FILE.write_text(json.dumps(grants, indent=2))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def evaluate_permission(agent_id: str, resource_type: str,
|
|
146
|
+
justification: str, scope: Optional[str] = None) -> dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Evaluate a permission request using weighted scoring.
|
|
149
|
+
|
|
150
|
+
Weights:
|
|
151
|
+
- Justification Quality: 40%
|
|
152
|
+
- Agent Trust Level: 30%
|
|
153
|
+
- Risk Assessment: 30%
|
|
154
|
+
"""
|
|
155
|
+
# Log the request
|
|
156
|
+
log_audit("permission_request", {
|
|
157
|
+
"agent_id": agent_id,
|
|
158
|
+
"resource_type": resource_type,
|
|
159
|
+
"justification": justification,
|
|
160
|
+
"scope": scope
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
# 1. Justification Quality (40% weight)
|
|
164
|
+
justification_score = score_justification(justification)
|
|
165
|
+
if justification_score < 0.3:
|
|
166
|
+
return {
|
|
167
|
+
"granted": False,
|
|
168
|
+
"reason": "Justification is insufficient. Please provide specific task context.",
|
|
169
|
+
"scores": {
|
|
170
|
+
"justification": justification_score,
|
|
171
|
+
"trust": None,
|
|
172
|
+
"risk": None
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# 2. Agent Trust Level (30% weight)
|
|
177
|
+
trust_level = DEFAULT_TRUST_LEVELS.get(agent_id, 0.5)
|
|
178
|
+
if trust_level < 0.4:
|
|
179
|
+
return {
|
|
180
|
+
"granted": False,
|
|
181
|
+
"reason": "Agent trust level is below threshold. Escalate to human operator.",
|
|
182
|
+
"scores": {
|
|
183
|
+
"justification": justification_score,
|
|
184
|
+
"trust": trust_level,
|
|
185
|
+
"risk": None
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# 3. Risk Assessment (30% weight)
|
|
190
|
+
risk_score = assess_risk(resource_type, scope)
|
|
191
|
+
if risk_score > 0.8:
|
|
192
|
+
return {
|
|
193
|
+
"granted": False,
|
|
194
|
+
"reason": "Risk assessment exceeds acceptable threshold. Narrow the requested scope.",
|
|
195
|
+
"scores": {
|
|
196
|
+
"justification": justification_score,
|
|
197
|
+
"trust": trust_level,
|
|
198
|
+
"risk": risk_score
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Calculate weighted approval score
|
|
203
|
+
weighted_score = (
|
|
204
|
+
justification_score * 0.4 +
|
|
205
|
+
trust_level * 0.3 +
|
|
206
|
+
(1 - risk_score) * 0.3
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if weighted_score < 0.5:
|
|
210
|
+
return {
|
|
211
|
+
"granted": False,
|
|
212
|
+
"reason": f"Combined evaluation score ({weighted_score:.2f}) below threshold (0.5).",
|
|
213
|
+
"scores": {
|
|
214
|
+
"justification": justification_score,
|
|
215
|
+
"trust": trust_level,
|
|
216
|
+
"risk": risk_score,
|
|
217
|
+
"weighted": weighted_score
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Generate grant
|
|
222
|
+
token = generate_grant_token()
|
|
223
|
+
expires_at = (datetime.now(timezone.utc) + timedelta(minutes=GRANT_TOKEN_TTL_MINUTES)).isoformat()
|
|
224
|
+
restrictions = RESTRICTIONS.get(resource_type, [])
|
|
225
|
+
|
|
226
|
+
grant: dict[str, Any] = {
|
|
227
|
+
"token": token,
|
|
228
|
+
"agent_id": agent_id,
|
|
229
|
+
"resource_type": resource_type,
|
|
230
|
+
"scope": scope,
|
|
231
|
+
"expires_at": expires_at,
|
|
232
|
+
"restrictions": restrictions,
|
|
233
|
+
"granted_at": datetime.now(timezone.utc).isoformat()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Save grant and log
|
|
237
|
+
save_grant(grant)
|
|
238
|
+
log_audit("permission_granted", grant)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"granted": True,
|
|
242
|
+
"token": token,
|
|
243
|
+
"expires_at": expires_at,
|
|
244
|
+
"restrictions": restrictions,
|
|
245
|
+
"scores": {
|
|
246
|
+
"justification": justification_score,
|
|
247
|
+
"trust": trust_level,
|
|
248
|
+
"risk": risk_score,
|
|
249
|
+
"weighted": weighted_score
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
parser = argparse.ArgumentParser(
|
|
256
|
+
description="AuthGuardian Permission Checker",
|
|
257
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
258
|
+
epilog="""
|
|
259
|
+
Examples:
|
|
260
|
+
%(prog)s --agent data_analyst --resource SAP_API \\
|
|
261
|
+
--justification "Need Q4 invoice data for quarterly report"
|
|
262
|
+
|
|
263
|
+
%(prog)s --agent orchestrator --resource FINANCIAL_API \\
|
|
264
|
+
--justification "Generating board presentation financials" \\
|
|
265
|
+
--scope "read:revenue,read:expenses"
|
|
266
|
+
"""
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
parser.add_argument(
|
|
270
|
+
"--agent", "-a",
|
|
271
|
+
required=True,
|
|
272
|
+
help="Agent ID requesting permission"
|
|
273
|
+
)
|
|
274
|
+
parser.add_argument(
|
|
275
|
+
"--resource", "-r",
|
|
276
|
+
required=True,
|
|
277
|
+
choices=["DATABASE", "PAYMENTS", "EMAIL", "FILE_EXPORT"],
|
|
278
|
+
help="Resource type to access"
|
|
279
|
+
)
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"--justification", "-j",
|
|
282
|
+
required=True,
|
|
283
|
+
help="Business justification for the request"
|
|
284
|
+
)
|
|
285
|
+
parser.add_argument(
|
|
286
|
+
"--scope", "-s",
|
|
287
|
+
help="Specific scope of access (e.g., 'read:invoices')"
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--json",
|
|
291
|
+
action="store_true",
|
|
292
|
+
help="Output result as JSON"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
args = parser.parse_args()
|
|
296
|
+
|
|
297
|
+
result = evaluate_permission(
|
|
298
|
+
agent_id=args.agent,
|
|
299
|
+
resource_type=args.resource,
|
|
300
|
+
justification=args.justification,
|
|
301
|
+
scope=args.scope
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if args.json:
|
|
305
|
+
print(json.dumps(result, indent=2))
|
|
306
|
+
else:
|
|
307
|
+
if result["granted"]:
|
|
308
|
+
print("✅ GRANTED")
|
|
309
|
+
print(f"Token: {result['token']}")
|
|
310
|
+
print(f"Expires: {result['expires_at']}")
|
|
311
|
+
print(f"Restrictions: {', '.join(result['restrictions'])}")
|
|
312
|
+
else:
|
|
313
|
+
print("❌ DENIED")
|
|
314
|
+
print(f"Reason: {result['reason']}")
|
|
315
|
+
|
|
316
|
+
print("\n📊 Evaluation Scores:")
|
|
317
|
+
scores = result["scores"]
|
|
318
|
+
if scores.get("justification") is not None:
|
|
319
|
+
print(f" Justification: {scores['justification']:.2f}")
|
|
320
|
+
if scores.get("trust") is not None:
|
|
321
|
+
print(f" Trust Level: {scores['trust']:.2f}")
|
|
322
|
+
if scores.get("risk") is not None:
|
|
323
|
+
print(f" Risk Score: {scores['risk']:.2f}")
|
|
324
|
+
if scores.get("weighted") is not None:
|
|
325
|
+
print(f" Weighted: {scores['weighted']:.2f}")
|
|
326
|
+
|
|
327
|
+
sys.exit(0 if result["granted"] else 1)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == "__main__":
|
|
331
|
+
main()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Revoke Grant Token & TTL Enforcement
|
|
4
|
+
|
|
5
|
+
Revoke permission tokens and automatically cleanup expired grants.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python revoke_token.py TOKEN # Revoke specific token
|
|
9
|
+
python revoke_token.py --cleanup # Remove all expired tokens
|
|
10
|
+
python revoke_token.py --list-expired # List expired tokens without removing
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
python revoke_token.py grant_a1b2c3d4e5f6
|
|
14
|
+
python revoke_token.py --cleanup --json
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
GRANTS_FILE = Path(__file__).parent.parent / "data" / "active_grants.json"
|
|
25
|
+
AUDIT_LOG = Path(__file__).parent.parent / "data" / "audit_log.jsonl"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def log_audit(action: str, details: dict[str, Any]) -> None:
|
|
29
|
+
"""Append entry to audit log."""
|
|
30
|
+
AUDIT_LOG.parent.mkdir(exist_ok=True)
|
|
31
|
+
entry: dict[str, Any] = {
|
|
32
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
33
|
+
"action": action,
|
|
34
|
+
"details": details
|
|
35
|
+
}
|
|
36
|
+
with open(AUDIT_LOG, "a") as f:
|
|
37
|
+
f.write(json.dumps(entry) + "\n")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def revoke_token(token: str) -> dict[str, Any]:
|
|
41
|
+
"""Revoke a grant token."""
|
|
42
|
+
if not GRANTS_FILE.exists():
|
|
43
|
+
return {
|
|
44
|
+
"revoked": False,
|
|
45
|
+
"reason": "No grants file found"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
grants = json.loads(GRANTS_FILE.read_text())
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return {
|
|
52
|
+
"revoked": False,
|
|
53
|
+
"reason": "Invalid grants file"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if token not in grants:
|
|
57
|
+
return {
|
|
58
|
+
"revoked": False,
|
|
59
|
+
"reason": "Token not found"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
grant = grants.pop(token)
|
|
63
|
+
GRANTS_FILE.write_text(json.dumps(grants, indent=2))
|
|
64
|
+
|
|
65
|
+
log_audit("permission_revoked", {
|
|
66
|
+
"token": token,
|
|
67
|
+
"original_grant": grant
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"revoked": True,
|
|
72
|
+
"grant": grant
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_token_expired(grant: dict[str, Any]) -> bool:
|
|
77
|
+
"""Check if a grant token has expired."""
|
|
78
|
+
expires_at = grant.get("expires_at")
|
|
79
|
+
if not expires_at:
|
|
80
|
+
return False
|
|
81
|
+
try:
|
|
82
|
+
expiry_time = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
|
83
|
+
return datetime.now(timezone.utc) > expiry_time
|
|
84
|
+
except (ValueError, AttributeError):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def list_expired_tokens() -> dict[str, Any]:
|
|
89
|
+
"""List all expired tokens without removing them."""
|
|
90
|
+
if not GRANTS_FILE.exists():
|
|
91
|
+
return {"expired_tokens": [], "total_grants": 0}
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
grants = json.loads(GRANTS_FILE.read_text())
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
return {"error": "Invalid grants file"}
|
|
97
|
+
|
|
98
|
+
expired: list[dict[str, Any]] = []
|
|
99
|
+
now = datetime.now(timezone.utc)
|
|
100
|
+
|
|
101
|
+
for token, grant in grants.items():
|
|
102
|
+
if is_token_expired(grant):
|
|
103
|
+
expired.append({
|
|
104
|
+
"token": token,
|
|
105
|
+
"agent": grant.get("agent_id"),
|
|
106
|
+
"resource": grant.get("resource_type"),
|
|
107
|
+
"expired_at": grant.get("expires_at")
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"expired_tokens": expired,
|
|
112
|
+
"expired_count": len(expired),
|
|
113
|
+
"total_grants": len(grants),
|
|
114
|
+
"checked_at": now.isoformat()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cleanup_expired_tokens() -> dict[str, Any]:
|
|
119
|
+
"""Remove all expired tokens from active grants (TTL enforcement)."""
|
|
120
|
+
if not GRANTS_FILE.exists():
|
|
121
|
+
return {
|
|
122
|
+
"cleaned": 0,
|
|
123
|
+
"remaining": 0,
|
|
124
|
+
"message": "No grants file found"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
grants = json.loads(GRANTS_FILE.read_text())
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
return {
|
|
131
|
+
"cleaned": 0,
|
|
132
|
+
"error": "Invalid grants file"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
original_count = len(grants)
|
|
136
|
+
expired_tokens: list[dict[str, Any]] = []
|
|
137
|
+
|
|
138
|
+
# Find and remove expired tokens
|
|
139
|
+
tokens_to_remove: list[str] = []
|
|
140
|
+
for token, grant in grants.items():
|
|
141
|
+
if is_token_expired(grant):
|
|
142
|
+
tokens_to_remove.append(token)
|
|
143
|
+
expired_tokens.append({
|
|
144
|
+
"token": token,
|
|
145
|
+
"agent": grant.get("agent_id"),
|
|
146
|
+
"resource": grant.get("resource_type"),
|
|
147
|
+
"expired_at": grant.get("expires_at")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
for token in tokens_to_remove:
|
|
151
|
+
grants.pop(token)
|
|
152
|
+
|
|
153
|
+
# Write back cleaned grants
|
|
154
|
+
GRANTS_FILE.write_text(json.dumps(grants, indent=2))
|
|
155
|
+
|
|
156
|
+
# Log the cleanup
|
|
157
|
+
if expired_tokens:
|
|
158
|
+
log_audit("ttl_cleanup", {
|
|
159
|
+
"expired_count": len(expired_tokens),
|
|
160
|
+
"expired_tokens": expired_tokens,
|
|
161
|
+
"remaining_grants": len(grants)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"cleaned": len(expired_tokens),
|
|
166
|
+
"expired_tokens": expired_tokens,
|
|
167
|
+
"remaining": len(grants),
|
|
168
|
+
"original_count": original_count,
|
|
169
|
+
"cleaned_at": datetime.now(timezone.utc).isoformat()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main():
|
|
174
|
+
parser = argparse.ArgumentParser(
|
|
175
|
+
description="Revoke permission grant tokens and enforce TTL cleanup"
|
|
176
|
+
)
|
|
177
|
+
parser.add_argument("token", nargs="?", help="Grant token to revoke")
|
|
178
|
+
parser.add_argument("--cleanup", action="store_true",
|
|
179
|
+
help="Remove all expired tokens (TTL enforcement)")
|
|
180
|
+
parser.add_argument("--list-expired", action="store_true",
|
|
181
|
+
help="List expired tokens without removing")
|
|
182
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
183
|
+
|
|
184
|
+
args = parser.parse_args()
|
|
185
|
+
|
|
186
|
+
# Handle --list-expired
|
|
187
|
+
if args.list_expired:
|
|
188
|
+
result = list_expired_tokens()
|
|
189
|
+
if args.json:
|
|
190
|
+
print(json.dumps(result, indent=2))
|
|
191
|
+
else:
|
|
192
|
+
expired = result.get("expired_tokens", [])
|
|
193
|
+
if expired:
|
|
194
|
+
print(f"⏰ Found {len(expired)} expired token(s):")
|
|
195
|
+
for t in expired:
|
|
196
|
+
print(f" • {t['token'][:20]}... ({t['agent']} → {t['resource']})")
|
|
197
|
+
print(f" Expired: {t['expired_at']}")
|
|
198
|
+
else:
|
|
199
|
+
print("✅ No expired tokens found")
|
|
200
|
+
print(f"\n Total grants: {result.get('total_grants', 0)}")
|
|
201
|
+
sys.exit(0)
|
|
202
|
+
|
|
203
|
+
# Handle --cleanup
|
|
204
|
+
if args.cleanup:
|
|
205
|
+
result = cleanup_expired_tokens()
|
|
206
|
+
if args.json:
|
|
207
|
+
print(json.dumps(result, indent=2))
|
|
208
|
+
else:
|
|
209
|
+
cleaned = result.get("cleaned", 0)
|
|
210
|
+
if cleaned > 0:
|
|
211
|
+
print(f"🧹 TTL Cleanup Complete")
|
|
212
|
+
print(f" Removed: {cleaned} expired token(s)")
|
|
213
|
+
for t in result.get("expired_tokens", []):
|
|
214
|
+
print(f" • {t['token'][:20]}... ({t['agent']})")
|
|
215
|
+
else:
|
|
216
|
+
print("✅ No expired tokens to clean")
|
|
217
|
+
print(f" Remaining active grants: {result.get('remaining', 0)}")
|
|
218
|
+
sys.exit(0)
|
|
219
|
+
|
|
220
|
+
# Handle single token revocation
|
|
221
|
+
if not args.token:
|
|
222
|
+
parser.print_help()
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
|
|
225
|
+
result = revoke_token(args.token)
|
|
226
|
+
|
|
227
|
+
if args.json:
|
|
228
|
+
print(json.dumps(result, indent=2))
|
|
229
|
+
else:
|
|
230
|
+
if result["revoked"]:
|
|
231
|
+
grant = result["grant"]
|
|
232
|
+
print("✅ Token REVOKED")
|
|
233
|
+
print(f" Agent: {grant.get('agent_id')}")
|
|
234
|
+
print(f" Resource: {grant.get('resource_type')}")
|
|
235
|
+
else:
|
|
236
|
+
print("❌ Revocation FAILED")
|
|
237
|
+
print(f" Reason: {result.get('reason')}")
|
|
238
|
+
|
|
239
|
+
sys.exit(0 if result.get("revoked") or result.get("cleaned", 0) >= 0 else 1)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
main()
|