agf-sdk 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.
- agf_sdk-0.1.0/PKG-INFO +217 -0
- agf_sdk-0.1.0/README.md +199 -0
- agf_sdk-0.1.0/agf/__init__.py +37 -0
- agf_sdk-0.1.0/agf/client.py +207 -0
- agf_sdk-0.1.0/agf/crewai.py +128 -0
- agf_sdk-0.1.0/agf/exceptions.py +57 -0
- agf_sdk-0.1.0/agf/govern.py +247 -0
- agf_sdk-0.1.0/agf/langchain.py +124 -0
- agf_sdk-0.1.0/agf/py.typed +0 -0
- agf_sdk-0.1.0/agf/sync.py +111 -0
- agf_sdk-0.1.0/agf/webhook.py +87 -0
- agf_sdk-0.1.0/pyproject.toml +37 -0
agf_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agf-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Governance Foundation — Python SDK
|
|
5
|
+
Project-URL: Homepage, https://agentgovernancefoundation.com
|
|
6
|
+
Project-URL: Documentation, https://agentgovernancefoundation.com/docs
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: all
|
|
11
|
+
Requires-Dist: crewai>=0.28; extra == 'all'
|
|
12
|
+
Requires-Dist: langchain-core>=0.2; extra == 'all'
|
|
13
|
+
Provides-Extra: crewai
|
|
14
|
+
Requires-Dist: crewai>=0.28; extra == 'crewai'
|
|
15
|
+
Provides-Extra: langchain
|
|
16
|
+
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# agf-sdk
|
|
20
|
+
|
|
21
|
+
Python SDK for the [Agent Governance Foundation](https://agentgovernancefoundation.com) authorization service. Enforce identity, trust, and policy controls on every action your AI agents take.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install agf-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
With LangChain support:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install agf-sdk[langchain]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
With CrewAI support:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install agf-sdk[crewai]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import os
|
|
45
|
+
from agf import AgentGovernance
|
|
46
|
+
|
|
47
|
+
agf = AgentGovernance(
|
|
48
|
+
api_key=os.environ["AGF_API_KEY"],
|
|
49
|
+
org_id="org_acme",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
result = agf.authorize(
|
|
53
|
+
agent_id="did:agf:agt_01abc",
|
|
54
|
+
action="file:write",
|
|
55
|
+
resource="s3://corp-data/q2.csv",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if result.allowed:
|
|
59
|
+
write_file()
|
|
60
|
+
else:
|
|
61
|
+
raise PermissionError(f"Denied: {result.reason}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Authorization results
|
|
65
|
+
|
|
66
|
+
`authorize()` never raises for deny/review — it always returns an `AuthResult`:
|
|
67
|
+
|
|
68
|
+
| Field | Type | Description |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `allowed` | `bool` | `True` when the PDP issued ALLOW |
|
|
71
|
+
| `denied` | `bool` | `True` when the PDP issued DENY |
|
|
72
|
+
| `review_required` | `bool` | `True` when HITL approval is needed |
|
|
73
|
+
| `reason` | `str` | Human-readable denial reason |
|
|
74
|
+
| `artifact_id` | `str` | Signed audit artifact ID |
|
|
75
|
+
| `risk_score` | `float` | 0.0–1.0 |
|
|
76
|
+
| `trust_score` | `int` | 0–100 |
|
|
77
|
+
| `approval_request_id` | `str` | HITL request ID (review_required only) |
|
|
78
|
+
|
|
79
|
+
## Async client
|
|
80
|
+
|
|
81
|
+
For async frameworks (FastAPI, async Django, etc.) use `AGFClient` directly:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from agf import AGFClient, AGFDeniedError
|
|
85
|
+
|
|
86
|
+
async def handle_request():
|
|
87
|
+
async with AGFClient(api_key="agfk_...") as client:
|
|
88
|
+
try:
|
|
89
|
+
result = await client.decide(
|
|
90
|
+
action_type="file:write",
|
|
91
|
+
resource="s3://corp-data/q2.csv",
|
|
92
|
+
chain=[root_jwt, agent_jwt],
|
|
93
|
+
)
|
|
94
|
+
except AGFDeniedError as exc:
|
|
95
|
+
print(f"Denied — artifact: {exc.artifact_id}")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## LangChain integration
|
|
99
|
+
|
|
100
|
+
### Authorization gate tool (recommended for most agents)
|
|
101
|
+
|
|
102
|
+
Add an authorization tool to your agent's tool list. The agent calls it before performing sensitive operations:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from agf import AgentGovernance
|
|
106
|
+
from langchain.agents import initialize_agent, AgentType
|
|
107
|
+
from langchain_openai import ChatOpenAI
|
|
108
|
+
|
|
109
|
+
agf = AgentGovernance(api_key="agfk_...", org_id="org_acme")
|
|
110
|
+
agf_tool = agf.langchain_tool(agent_id="did:agf:agt_01abc")
|
|
111
|
+
|
|
112
|
+
agent = initialize_agent(
|
|
113
|
+
tools=[agf_tool, *your_other_tools],
|
|
114
|
+
llm=ChatOpenAI(),
|
|
115
|
+
agent=AgentType.OPENAI_FUNCTIONS,
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Per-tool guard (enforces policy on every tool call)
|
|
120
|
+
|
|
121
|
+
Wrap individual tools so no call can bypass the policy check:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from langchain_community.tools import ShellTool
|
|
125
|
+
from agf.langchain import AGFGuardedTool
|
|
126
|
+
from agf import AGFClient
|
|
127
|
+
|
|
128
|
+
client = AGFClient(api_key="agfk_...")
|
|
129
|
+
|
|
130
|
+
guarded_shell = AGFGuardedTool(
|
|
131
|
+
tool=ShellTool(),
|
|
132
|
+
client=client,
|
|
133
|
+
agent_id="did:agf:my-assistant",
|
|
134
|
+
action_type="exec:shell",
|
|
135
|
+
resource="local-shell",
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## CrewAI integration
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from crewai import Agent
|
|
143
|
+
from crewai.tools import BaseTool as CrewBaseTool
|
|
144
|
+
from agf.crewai import AGFCrewAITool
|
|
145
|
+
from agf import AGFClient
|
|
146
|
+
|
|
147
|
+
client = AGFClient(api_key="agfk_...")
|
|
148
|
+
|
|
149
|
+
class MyDBTool(CrewBaseTool):
|
|
150
|
+
name: str = "database_query"
|
|
151
|
+
description: str = "Query the production database"
|
|
152
|
+
|
|
153
|
+
def _run(self, query: str) -> str:
|
|
154
|
+
return db.execute(query)
|
|
155
|
+
|
|
156
|
+
guarded = AGFCrewAITool(
|
|
157
|
+
tool=MyDBTool(),
|
|
158
|
+
client=client,
|
|
159
|
+
agent_id="did:agf:crew-researcher",
|
|
160
|
+
action_type="query:database",
|
|
161
|
+
resource="prod-db",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
crew_agent = Agent(tools=[guarded], ...)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Webhook verification
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from agf import verify_signature, parse_event, AGFWebhookVerificationError
|
|
171
|
+
|
|
172
|
+
# FastAPI example
|
|
173
|
+
from fastapi import FastAPI, Request, HTTPException
|
|
174
|
+
|
|
175
|
+
app = FastAPI()
|
|
176
|
+
|
|
177
|
+
@app.post("/agf-webhook")
|
|
178
|
+
async def handle(request: Request):
|
|
179
|
+
body = await request.body()
|
|
180
|
+
try:
|
|
181
|
+
verify_signature(body, request.headers["X-AGF-Signature"], WEBHOOK_SECRET)
|
|
182
|
+
except AGFWebhookVerificationError:
|
|
183
|
+
raise HTTPException(status_code=400, detail="Invalid signature")
|
|
184
|
+
|
|
185
|
+
event = parse_event(body)
|
|
186
|
+
if event.type == "decision.deny":
|
|
187
|
+
print(f"Agent {event.agent_id} was denied — artifact {event.artifact_id}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Sync client
|
|
191
|
+
|
|
192
|
+
For scripts, Django views, or any non-async context:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from agf import SyncAGFClient
|
|
196
|
+
|
|
197
|
+
with SyncAGFClient(api_key="agfk_...") as client:
|
|
198
|
+
result = client.decide("file:write", "s3://bucket/file.csv")
|
|
199
|
+
agents = client.list_agents(status="active")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Environment variable
|
|
203
|
+
|
|
204
|
+
Set `AGF_API_KEY` in your environment and pass it via `os.environ["AGF_API_KEY"]`. The SDK does not auto-read environment variables — this keeps the dependency graph minimal and the behaviour explicit.
|
|
205
|
+
|
|
206
|
+
## Requirements
|
|
207
|
+
|
|
208
|
+
- Python 3.10+
|
|
209
|
+
- `httpx >= 0.27`
|
|
210
|
+
- `langchain-core >= 0.2` (optional, `agf-sdk[langchain]`)
|
|
211
|
+
- `crewai >= 0.28` (optional, `agf-sdk[crewai]`)
|
|
212
|
+
|
|
213
|
+
## Links
|
|
214
|
+
|
|
215
|
+
- [Documentation](https://agentgovernancefoundation.com/docs)
|
|
216
|
+
- [Quick start](https://agentgovernancefoundation.com/docs/quick-start)
|
|
217
|
+
- [API reference](https://agentgovernancefoundation.com/docs/api)
|
agf_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# agf-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Agent Governance Foundation](https://agentgovernancefoundation.com) authorization service. Enforce identity, trust, and policy controls on every action your AI agents take.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agf-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
With LangChain support:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install agf-sdk[langchain]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
With CrewAI support:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install agf-sdk[crewai]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import os
|
|
27
|
+
from agf import AgentGovernance
|
|
28
|
+
|
|
29
|
+
agf = AgentGovernance(
|
|
30
|
+
api_key=os.environ["AGF_API_KEY"],
|
|
31
|
+
org_id="org_acme",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
result = agf.authorize(
|
|
35
|
+
agent_id="did:agf:agt_01abc",
|
|
36
|
+
action="file:write",
|
|
37
|
+
resource="s3://corp-data/q2.csv",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if result.allowed:
|
|
41
|
+
write_file()
|
|
42
|
+
else:
|
|
43
|
+
raise PermissionError(f"Denied: {result.reason}")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Authorization results
|
|
47
|
+
|
|
48
|
+
`authorize()` never raises for deny/review — it always returns an `AuthResult`:
|
|
49
|
+
|
|
50
|
+
| Field | Type | Description |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `allowed` | `bool` | `True` when the PDP issued ALLOW |
|
|
53
|
+
| `denied` | `bool` | `True` when the PDP issued DENY |
|
|
54
|
+
| `review_required` | `bool` | `True` when HITL approval is needed |
|
|
55
|
+
| `reason` | `str` | Human-readable denial reason |
|
|
56
|
+
| `artifact_id` | `str` | Signed audit artifact ID |
|
|
57
|
+
| `risk_score` | `float` | 0.0–1.0 |
|
|
58
|
+
| `trust_score` | `int` | 0–100 |
|
|
59
|
+
| `approval_request_id` | `str` | HITL request ID (review_required only) |
|
|
60
|
+
|
|
61
|
+
## Async client
|
|
62
|
+
|
|
63
|
+
For async frameworks (FastAPI, async Django, etc.) use `AGFClient` directly:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from agf import AGFClient, AGFDeniedError
|
|
67
|
+
|
|
68
|
+
async def handle_request():
|
|
69
|
+
async with AGFClient(api_key="agfk_...") as client:
|
|
70
|
+
try:
|
|
71
|
+
result = await client.decide(
|
|
72
|
+
action_type="file:write",
|
|
73
|
+
resource="s3://corp-data/q2.csv",
|
|
74
|
+
chain=[root_jwt, agent_jwt],
|
|
75
|
+
)
|
|
76
|
+
except AGFDeniedError as exc:
|
|
77
|
+
print(f"Denied — artifact: {exc.artifact_id}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## LangChain integration
|
|
81
|
+
|
|
82
|
+
### Authorization gate tool (recommended for most agents)
|
|
83
|
+
|
|
84
|
+
Add an authorization tool to your agent's tool list. The agent calls it before performing sensitive operations:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from agf import AgentGovernance
|
|
88
|
+
from langchain.agents import initialize_agent, AgentType
|
|
89
|
+
from langchain_openai import ChatOpenAI
|
|
90
|
+
|
|
91
|
+
agf = AgentGovernance(api_key="agfk_...", org_id="org_acme")
|
|
92
|
+
agf_tool = agf.langchain_tool(agent_id="did:agf:agt_01abc")
|
|
93
|
+
|
|
94
|
+
agent = initialize_agent(
|
|
95
|
+
tools=[agf_tool, *your_other_tools],
|
|
96
|
+
llm=ChatOpenAI(),
|
|
97
|
+
agent=AgentType.OPENAI_FUNCTIONS,
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Per-tool guard (enforces policy on every tool call)
|
|
102
|
+
|
|
103
|
+
Wrap individual tools so no call can bypass the policy check:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from langchain_community.tools import ShellTool
|
|
107
|
+
from agf.langchain import AGFGuardedTool
|
|
108
|
+
from agf import AGFClient
|
|
109
|
+
|
|
110
|
+
client = AGFClient(api_key="agfk_...")
|
|
111
|
+
|
|
112
|
+
guarded_shell = AGFGuardedTool(
|
|
113
|
+
tool=ShellTool(),
|
|
114
|
+
client=client,
|
|
115
|
+
agent_id="did:agf:my-assistant",
|
|
116
|
+
action_type="exec:shell",
|
|
117
|
+
resource="local-shell",
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## CrewAI integration
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from crewai import Agent
|
|
125
|
+
from crewai.tools import BaseTool as CrewBaseTool
|
|
126
|
+
from agf.crewai import AGFCrewAITool
|
|
127
|
+
from agf import AGFClient
|
|
128
|
+
|
|
129
|
+
client = AGFClient(api_key="agfk_...")
|
|
130
|
+
|
|
131
|
+
class MyDBTool(CrewBaseTool):
|
|
132
|
+
name: str = "database_query"
|
|
133
|
+
description: str = "Query the production database"
|
|
134
|
+
|
|
135
|
+
def _run(self, query: str) -> str:
|
|
136
|
+
return db.execute(query)
|
|
137
|
+
|
|
138
|
+
guarded = AGFCrewAITool(
|
|
139
|
+
tool=MyDBTool(),
|
|
140
|
+
client=client,
|
|
141
|
+
agent_id="did:agf:crew-researcher",
|
|
142
|
+
action_type="query:database",
|
|
143
|
+
resource="prod-db",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
crew_agent = Agent(tools=[guarded], ...)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Webhook verification
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from agf import verify_signature, parse_event, AGFWebhookVerificationError
|
|
153
|
+
|
|
154
|
+
# FastAPI example
|
|
155
|
+
from fastapi import FastAPI, Request, HTTPException
|
|
156
|
+
|
|
157
|
+
app = FastAPI()
|
|
158
|
+
|
|
159
|
+
@app.post("/agf-webhook")
|
|
160
|
+
async def handle(request: Request):
|
|
161
|
+
body = await request.body()
|
|
162
|
+
try:
|
|
163
|
+
verify_signature(body, request.headers["X-AGF-Signature"], WEBHOOK_SECRET)
|
|
164
|
+
except AGFWebhookVerificationError:
|
|
165
|
+
raise HTTPException(status_code=400, detail="Invalid signature")
|
|
166
|
+
|
|
167
|
+
event = parse_event(body)
|
|
168
|
+
if event.type == "decision.deny":
|
|
169
|
+
print(f"Agent {event.agent_id} was denied — artifact {event.artifact_id}")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Sync client
|
|
173
|
+
|
|
174
|
+
For scripts, Django views, or any non-async context:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from agf import SyncAGFClient
|
|
178
|
+
|
|
179
|
+
with SyncAGFClient(api_key="agfk_...") as client:
|
|
180
|
+
result = client.decide("file:write", "s3://bucket/file.csv")
|
|
181
|
+
agents = client.list_agents(status="active")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Environment variable
|
|
185
|
+
|
|
186
|
+
Set `AGF_API_KEY` in your environment and pass it via `os.environ["AGF_API_KEY"]`. The SDK does not auto-read environment variables — this keeps the dependency graph minimal and the behaviour explicit.
|
|
187
|
+
|
|
188
|
+
## Requirements
|
|
189
|
+
|
|
190
|
+
- Python 3.10+
|
|
191
|
+
- `httpx >= 0.27`
|
|
192
|
+
- `langchain-core >= 0.2` (optional, `agf-sdk[langchain]`)
|
|
193
|
+
- `crewai >= 0.28` (optional, `agf-sdk[crewai]`)
|
|
194
|
+
|
|
195
|
+
## Links
|
|
196
|
+
|
|
197
|
+
- [Documentation](https://agentgovernancefoundation.com/docs)
|
|
198
|
+
- [Quick start](https://agentgovernancefoundation.com/docs/quick-start)
|
|
199
|
+
- [API reference](https://agentgovernancefoundation.com/docs/api)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Agent Governance Foundation Python SDK."""
|
|
2
|
+
from .client import AGFClient, Agent, DecisionResult
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
AGFAuthError,
|
|
5
|
+
AGFConnectionError,
|
|
6
|
+
AGFDeniedError,
|
|
7
|
+
AGFError,
|
|
8
|
+
AGFReviewRequiredError,
|
|
9
|
+
)
|
|
10
|
+
from .govern import AgentGovernance, AuthResult
|
|
11
|
+
from .sync import SyncAGFClient
|
|
12
|
+
from .webhook import AGFWebhookVerificationError, WebhookEvent, parse_event, verify_signature
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
# High-level facade (most users start here)
|
|
16
|
+
"AgentGovernance",
|
|
17
|
+
"AuthResult",
|
|
18
|
+
# Async client
|
|
19
|
+
"AGFClient",
|
|
20
|
+
"Agent",
|
|
21
|
+
"DecisionResult",
|
|
22
|
+
# Sync client
|
|
23
|
+
"SyncAGFClient",
|
|
24
|
+
# Exceptions
|
|
25
|
+
"AGFError",
|
|
26
|
+
"AGFAuthError",
|
|
27
|
+
"AGFConnectionError",
|
|
28
|
+
"AGFDeniedError",
|
|
29
|
+
"AGFReviewRequiredError",
|
|
30
|
+
# Webhook
|
|
31
|
+
"AGFWebhookVerificationError",
|
|
32
|
+
"WebhookEvent",
|
|
33
|
+
"verify_signature",
|
|
34
|
+
"parse_event",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""AGF Python SDK — async client."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import AGFAuthError, AGFConnectionError, AGFDeniedError, AGFReviewRequiredError
|
|
10
|
+
|
|
11
|
+
_DEFAULT_BASE_URL = "https://api.agentgovernancefoundation.com"
|
|
12
|
+
_DEFAULT_TIMEOUT = 15.0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DecisionResult:
|
|
17
|
+
"""Returned by :meth:`AGFClient.decide` on an ALLOW decision."""
|
|
18
|
+
decision: str
|
|
19
|
+
artifact_id: str
|
|
20
|
+
trust_score: int
|
|
21
|
+
risk_score: float
|
|
22
|
+
policy_version: str
|
|
23
|
+
chain_depth: int
|
|
24
|
+
effective_scope: list[str]
|
|
25
|
+
reasoning: list[str]
|
|
26
|
+
approval_request_id: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Agent:
|
|
31
|
+
id: str
|
|
32
|
+
org_id: str
|
|
33
|
+
name: str
|
|
34
|
+
did: str
|
|
35
|
+
status: str
|
|
36
|
+
trust_score: int | None = None
|
|
37
|
+
created_at: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AGFClient:
|
|
42
|
+
"""Async AGF runtime client.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
api_key: Org-level API key from Settings → API Keys.
|
|
46
|
+
base_url: Override the default AGF runtime URL.
|
|
47
|
+
timeout: HTTP timeout in seconds.
|
|
48
|
+
|
|
49
|
+
Example::
|
|
50
|
+
|
|
51
|
+
client = AGFClient(api_key="ak_live_...")
|
|
52
|
+
result = await client.decide(
|
|
53
|
+
action_type="write:database",
|
|
54
|
+
resource="prod-customers",
|
|
55
|
+
chain=[root_jwt, agent_jwt],
|
|
56
|
+
)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
api_key: str
|
|
60
|
+
base_url: str = _DEFAULT_BASE_URL
|
|
61
|
+
timeout: float = _DEFAULT_TIMEOUT
|
|
62
|
+
_http: httpx.AsyncClient = field(init=False, repr=False)
|
|
63
|
+
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
self._http = httpx.AsyncClient(
|
|
66
|
+
base_url=self.base_url.rstrip("/"),
|
|
67
|
+
headers={
|
|
68
|
+
"X-AGF-Key": self.api_key,
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"User-Agent": "agf-sdk/0.1.0 python",
|
|
71
|
+
},
|
|
72
|
+
timeout=self._timeout_obj,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def _timeout_obj(self) -> httpx.Timeout:
|
|
77
|
+
return httpx.Timeout(self.timeout)
|
|
78
|
+
|
|
79
|
+
async def __aenter__(self) -> "AGFClient":
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
83
|
+
await self.aclose()
|
|
84
|
+
|
|
85
|
+
async def aclose(self) -> None:
|
|
86
|
+
await self._http.aclose()
|
|
87
|
+
|
|
88
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
89
|
+
try:
|
|
90
|
+
resp = await self._http.request(method, path, **kwargs)
|
|
91
|
+
except httpx.ConnectError as exc:
|
|
92
|
+
raise AGFConnectionError(f"Cannot connect to AGF runtime at {self.base_url}: {exc}") from exc
|
|
93
|
+
except httpx.TimeoutException as exc:
|
|
94
|
+
raise AGFConnectionError(f"AGF runtime request timed out: {exc}") from exc
|
|
95
|
+
|
|
96
|
+
if resp.status_code == 401:
|
|
97
|
+
raise AGFAuthError("Invalid API key or suspended organisation.")
|
|
98
|
+
if resp.status_code == 403:
|
|
99
|
+
raise AGFAuthError("Insufficient permissions.")
|
|
100
|
+
|
|
101
|
+
resp.raise_for_status()
|
|
102
|
+
return resp.json() # type: ignore[no-any-return]
|
|
103
|
+
|
|
104
|
+
async def decide(
|
|
105
|
+
self,
|
|
106
|
+
action_type: str,
|
|
107
|
+
resource: str,
|
|
108
|
+
*,
|
|
109
|
+
chain: list[str] | None = None,
|
|
110
|
+
audience: str = "agf",
|
|
111
|
+
context: dict[str, Any] | None = None,
|
|
112
|
+
policy_version: str | None = None,
|
|
113
|
+
) -> DecisionResult:
|
|
114
|
+
"""Evaluate an action against the AGF policy engine.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
AGFDeniedError: The PDP issued a DENY decision.
|
|
118
|
+
AGFReviewRequiredError: The action needs human review (HITL).
|
|
119
|
+
AGFConnectionError: Cannot reach the AGF runtime.
|
|
120
|
+
AGFAuthError: API key is invalid.
|
|
121
|
+
"""
|
|
122
|
+
body: dict[str, Any] = {
|
|
123
|
+
"chain": chain or [],
|
|
124
|
+
"action": {"type": action_type, "resource": resource},
|
|
125
|
+
"audience": audience,
|
|
126
|
+
}
|
|
127
|
+
if context:
|
|
128
|
+
body["context"] = context
|
|
129
|
+
if policy_version:
|
|
130
|
+
body["policy_version"] = policy_version
|
|
131
|
+
|
|
132
|
+
data = await self._request("POST", "/v1/decide", json=body)
|
|
133
|
+
decision_data = data.get("data", data)
|
|
134
|
+
decision = decision_data.get("decision", "DENY")
|
|
135
|
+
artifact_id = decision_data.get("artifact_id", "")
|
|
136
|
+
|
|
137
|
+
if decision == "DENY":
|
|
138
|
+
raise AGFDeniedError(
|
|
139
|
+
f"Action '{action_type}' on '{resource}' was denied by policy.",
|
|
140
|
+
artifact_id=artifact_id,
|
|
141
|
+
risk_score=decision_data.get("risk_score", 0.0),
|
|
142
|
+
reasoning=decision_data.get("reasoning", []),
|
|
143
|
+
)
|
|
144
|
+
if decision == "REVIEW_REQUIRED":
|
|
145
|
+
raise AGFReviewRequiredError(
|
|
146
|
+
f"Action '{action_type}' on '{resource}' requires human approval.",
|
|
147
|
+
approval_request_id=decision_data.get("approval_request_id") or "",
|
|
148
|
+
artifact_id=artifact_id,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return DecisionResult(
|
|
152
|
+
decision=decision,
|
|
153
|
+
artifact_id=artifact_id,
|
|
154
|
+
trust_score=decision_data.get("trust_score", 0),
|
|
155
|
+
risk_score=decision_data.get("risk_score", 0.0),
|
|
156
|
+
policy_version=decision_data.get("policy_version", ""),
|
|
157
|
+
chain_depth=decision_data.get("chain_depth", 0),
|
|
158
|
+
effective_scope=decision_data.get("effective_scope", []),
|
|
159
|
+
reasoning=decision_data.get("reasoning", []),
|
|
160
|
+
approval_request_id=decision_data.get("approval_request_id"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
async def list_agents(
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
status: str | None = None,
|
|
167
|
+
page: int = 1,
|
|
168
|
+
per_page: int = 50,
|
|
169
|
+
) -> list[Agent]:
|
|
170
|
+
params: dict[str, Any] = {"page": page, "per_page": per_page}
|
|
171
|
+
if status:
|
|
172
|
+
params["status"] = status
|
|
173
|
+
data = await self._request("GET", "/v1/agents", params=params)
|
|
174
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
175
|
+
return [
|
|
176
|
+
Agent(
|
|
177
|
+
id=a["id"],
|
|
178
|
+
org_id=a.get("org_id", ""),
|
|
179
|
+
name=a.get("name", ""),
|
|
180
|
+
did=a.get("did", ""),
|
|
181
|
+
status=a.get("status", "active"),
|
|
182
|
+
trust_score=a.get("trust_score"),
|
|
183
|
+
created_at=a.get("created_at", ""),
|
|
184
|
+
)
|
|
185
|
+
for a in items
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
async def register_agent(
|
|
189
|
+
self,
|
|
190
|
+
name: str,
|
|
191
|
+
did: str,
|
|
192
|
+
public_key_pem: str,
|
|
193
|
+
metadata: dict[str, Any] | None = None,
|
|
194
|
+
) -> Agent:
|
|
195
|
+
body: dict[str, Any] = {"name": name, "did": did, "public_key_pem": public_key_pem}
|
|
196
|
+
if metadata:
|
|
197
|
+
body["metadata"] = metadata
|
|
198
|
+
a = await self._request("POST", "/v1/agents", json=body)
|
|
199
|
+
return Agent(
|
|
200
|
+
id=a["id"],
|
|
201
|
+
org_id=a.get("org_id", ""),
|
|
202
|
+
name=a.get("name", name),
|
|
203
|
+
did=a.get("did", did),
|
|
204
|
+
status=a.get("status", "active"),
|
|
205
|
+
trust_score=a.get("trust_score"),
|
|
206
|
+
created_at=a.get("created_at", ""),
|
|
207
|
+
)
|