aigp-server 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.
- aigp_server-0.1.0/.gitignore +94 -0
- aigp_server-0.1.0/PKG-INFO +99 -0
- aigp_server-0.1.0/README.md +75 -0
- aigp_server-0.1.0/aigp_server/__init__.py +38 -0
- aigp_server-0.1.0/aigp_server/circuit_breaker.py +102 -0
- aigp_server-0.1.0/aigp_server/governance_engine.py +165 -0
- aigp_server-0.1.0/aigp_server/hmac_auth.py +54 -0
- aigp_server-0.1.0/aigp_server/routes.py +78 -0
- aigp_server-0.1.0/aigp_server/scope_manager.py +99 -0
- aigp_server-0.1.0/aigp_server/store.py +78 -0
- aigp_server-0.1.0/pyproject.toml +32 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Kiro spec/session artifacts — local only
|
|
2
|
+
.kiro/
|
|
3
|
+
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
# Python
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.so
|
|
11
|
+
.Python
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
ENV/
|
|
18
|
+
|
|
19
|
+
# Packaging / build artifacts
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
*.egg
|
|
24
|
+
*.whl
|
|
25
|
+
*.tar.gz
|
|
26
|
+
pip-wheel-metadata/
|
|
27
|
+
|
|
28
|
+
# Test / tooling caches
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
.tox/
|
|
33
|
+
.coverage
|
|
34
|
+
.coverage.*
|
|
35
|
+
htmlcov/
|
|
36
|
+
coverage.xml
|
|
37
|
+
*.cover
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Node / TypeScript
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
node_modules/
|
|
43
|
+
clients/typescript/dist/
|
|
44
|
+
*.tsbuildinfo
|
|
45
|
+
npm-debug.log*
|
|
46
|
+
yarn-debug.log*
|
|
47
|
+
yarn-error.log*
|
|
48
|
+
.pnpm-debug.log*
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Other language build outputs
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Java / Maven
|
|
54
|
+
clients/java/target/
|
|
55
|
+
|
|
56
|
+
# Rust
|
|
57
|
+
clients/rust/target/
|
|
58
|
+
Cargo.lock
|
|
59
|
+
|
|
60
|
+
# Go
|
|
61
|
+
clients/go/vendor/
|
|
62
|
+
|
|
63
|
+
# .NET / C#
|
|
64
|
+
clients/csharp/bin/
|
|
65
|
+
clients/csharp/obj/
|
|
66
|
+
|
|
67
|
+
# Ruby
|
|
68
|
+
clients/ruby/pkg/
|
|
69
|
+
*.gem
|
|
70
|
+
|
|
71
|
+
# Swift
|
|
72
|
+
.build/
|
|
73
|
+
Packages/
|
|
74
|
+
*.xcodeproj/
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Editor / OS
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
.DS_Store
|
|
80
|
+
Thumbs.db
|
|
81
|
+
.idea/
|
|
82
|
+
.vscode/
|
|
83
|
+
*.swp
|
|
84
|
+
*.swo
|
|
85
|
+
*~
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Secrets / local config
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
.env
|
|
91
|
+
.env.*
|
|
92
|
+
!.env.example
|
|
93
|
+
*.pem
|
|
94
|
+
*.key
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aigp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AIGP Governance Server — agentic governance engine (scope envelopes, circuit breakers, delegation)
|
|
5
|
+
Project-URL: Homepage, https://github.com/owner-spec/aigp-protocol
|
|
6
|
+
Project-URL: Repository, https://github.com/owner-spec/aigp-protocol
|
|
7
|
+
Author-email: Evan Erwee <evan@erwee.com>
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Keywords: agentic,ai,aigp,circuit-breaker,governance,protocol,scope-envelope
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# aigp-server — AIGP Governance Engine
|
|
26
|
+
|
|
27
|
+
Server-side governance engine for the AI Governance Protocol (AIGP). Evaluates agentic governance decisions: tool authorization, plan approval, delegation with scope narrowing, circuit breakers, and memory governance.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install aigp-server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from aigp_server import (
|
|
39
|
+
GovernanceEngine, GovernanceStore, AigpRouter,
|
|
40
|
+
ScopeEnvelopeManager, CircuitBreakerService,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 1. Implement the storage interface for your DB
|
|
44
|
+
class MyStore(GovernanceStore):
|
|
45
|
+
async def put_scope_envelope(self, envelope: dict) -> None: ...
|
|
46
|
+
async def get_active_scope(self, agent_id: str) -> dict | None: ...
|
|
47
|
+
# ... (see store.py for full interface)
|
|
48
|
+
|
|
49
|
+
# 2. Wire up the engine
|
|
50
|
+
store = MyStore()
|
|
51
|
+
scope_mgr = ScopeEnvelopeManager(store)
|
|
52
|
+
cb = CircuitBreakerService(store)
|
|
53
|
+
engine = GovernanceEngine(store, scope_mgr, cb, mode="ENFORCE")
|
|
54
|
+
|
|
55
|
+
# 3. Create the router (framework-agnostic)
|
|
56
|
+
router = AigpRouter(engine, hmac_secret="your-secret")
|
|
57
|
+
|
|
58
|
+
# 4. Handle requests — returns (status_code, response_dict)
|
|
59
|
+
status, response = await router.handle_tool_request(headers_dict, body_bytes)
|
|
60
|
+
status, response = await router.handle_plan_submit(headers_dict, body_bytes)
|
|
61
|
+
status, response = await router.handle_escalate(headers_dict, body_bytes)
|
|
62
|
+
status, response = await router.handle_delegate(headers_dict, body_bytes)
|
|
63
|
+
status, response = await router.handle_memory_write(headers_dict, body_bytes)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Architecture
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
aigp-server/
|
|
70
|
+
store.py — GovernanceStore ABC (implement for your DB)
|
|
71
|
+
governance_engine.py — Core decision engine (6 handlers)
|
|
72
|
+
scope_manager.py — Scope envelope lifecycle + SoD + templates
|
|
73
|
+
circuit_breaker.py — 3-state machine with cascading halt
|
|
74
|
+
routes.py — Framework-agnostic router (HMAC + dispatch)
|
|
75
|
+
hmac_auth.py — HMAC-SHA256 verify/sign utilities
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Handlers
|
|
79
|
+
|
|
80
|
+
| Handler | RFC §15 | Decision |
|
|
81
|
+
|---------|---------|----------|
|
|
82
|
+
| `handle_tool_request` | §15.6 | ALLOW / DENY (scope + budget + circuit breaker) |
|
|
83
|
+
| `handle_plan_submit` | §15.8 | APPROVED / APPROVED_WITH_MODIFICATIONS / REJECTED |
|
|
84
|
+
| `handle_step_complete` | — | Budget decrement + circuit breaker outcome |
|
|
85
|
+
| `handle_escalate` | §15.9 | Creates pending task for human review |
|
|
86
|
+
| `handle_delegate` | §15.10 | Scope narrowing (A ∩ B), depth limit (max 5) |
|
|
87
|
+
| `handle_memory_write` | §15.13 | Classification + retention + isolation check |
|
|
88
|
+
|
|
89
|
+
## Modes
|
|
90
|
+
|
|
91
|
+
| Mode | Behavior |
|
|
92
|
+
|------|----------|
|
|
93
|
+
| `REPORT` | Log denials but return ALLOW (shadow mode) |
|
|
94
|
+
| `REPORT-TRACE` | Same as REPORT + emit trace telemetry |
|
|
95
|
+
| `ENFORCE` | Deny violations (fail-closed) |
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
Proprietary — © 2025-2026 Evan Erwee. All rights reserved.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# aigp-server — AIGP Governance Engine
|
|
2
|
+
|
|
3
|
+
Server-side governance engine for the AI Governance Protocol (AIGP). Evaluates agentic governance decisions: tool authorization, plan approval, delegation with scope narrowing, circuit breakers, and memory governance.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install aigp-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from aigp_server import (
|
|
15
|
+
GovernanceEngine, GovernanceStore, AigpRouter,
|
|
16
|
+
ScopeEnvelopeManager, CircuitBreakerService,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# 1. Implement the storage interface for your DB
|
|
20
|
+
class MyStore(GovernanceStore):
|
|
21
|
+
async def put_scope_envelope(self, envelope: dict) -> None: ...
|
|
22
|
+
async def get_active_scope(self, agent_id: str) -> dict | None: ...
|
|
23
|
+
# ... (see store.py for full interface)
|
|
24
|
+
|
|
25
|
+
# 2. Wire up the engine
|
|
26
|
+
store = MyStore()
|
|
27
|
+
scope_mgr = ScopeEnvelopeManager(store)
|
|
28
|
+
cb = CircuitBreakerService(store)
|
|
29
|
+
engine = GovernanceEngine(store, scope_mgr, cb, mode="ENFORCE")
|
|
30
|
+
|
|
31
|
+
# 3. Create the router (framework-agnostic)
|
|
32
|
+
router = AigpRouter(engine, hmac_secret="your-secret")
|
|
33
|
+
|
|
34
|
+
# 4. Handle requests — returns (status_code, response_dict)
|
|
35
|
+
status, response = await router.handle_tool_request(headers_dict, body_bytes)
|
|
36
|
+
status, response = await router.handle_plan_submit(headers_dict, body_bytes)
|
|
37
|
+
status, response = await router.handle_escalate(headers_dict, body_bytes)
|
|
38
|
+
status, response = await router.handle_delegate(headers_dict, body_bytes)
|
|
39
|
+
status, response = await router.handle_memory_write(headers_dict, body_bytes)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
aigp-server/
|
|
46
|
+
store.py — GovernanceStore ABC (implement for your DB)
|
|
47
|
+
governance_engine.py — Core decision engine (6 handlers)
|
|
48
|
+
scope_manager.py — Scope envelope lifecycle + SoD + templates
|
|
49
|
+
circuit_breaker.py — 3-state machine with cascading halt
|
|
50
|
+
routes.py — Framework-agnostic router (HMAC + dispatch)
|
|
51
|
+
hmac_auth.py — HMAC-SHA256 verify/sign utilities
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Handlers
|
|
55
|
+
|
|
56
|
+
| Handler | RFC §15 | Decision |
|
|
57
|
+
|---------|---------|----------|
|
|
58
|
+
| `handle_tool_request` | §15.6 | ALLOW / DENY (scope + budget + circuit breaker) |
|
|
59
|
+
| `handle_plan_submit` | §15.8 | APPROVED / APPROVED_WITH_MODIFICATIONS / REJECTED |
|
|
60
|
+
| `handle_step_complete` | — | Budget decrement + circuit breaker outcome |
|
|
61
|
+
| `handle_escalate` | §15.9 | Creates pending task for human review |
|
|
62
|
+
| `handle_delegate` | §15.10 | Scope narrowing (A ∩ B), depth limit (max 5) |
|
|
63
|
+
| `handle_memory_write` | §15.13 | Classification + retention + isolation check |
|
|
64
|
+
|
|
65
|
+
## Modes
|
|
66
|
+
|
|
67
|
+
| Mode | Behavior |
|
|
68
|
+
|------|----------|
|
|
69
|
+
| `REPORT` | Log denials but return ALLOW (shadow mode) |
|
|
70
|
+
| `REPORT-TRACE` | Same as REPORT + emit trace telemetry |
|
|
71
|
+
| `ENFORCE` | Deny violations (fail-closed) |
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Proprietary — © 2025-2026 Evan Erwee. All rights reserved.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AIGP Server — Agentic Governance Engine package.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from aigp_server import GovernanceEngine, GovernanceStore, AigpRouter
|
|
5
|
+
|
|
6
|
+
# Implement GovernanceStore for your DB:
|
|
7
|
+
class MyStore(GovernanceStore): ...
|
|
8
|
+
|
|
9
|
+
# Wire up:
|
|
10
|
+
store = MyStore()
|
|
11
|
+
scope_mgr = ScopeEnvelopeManager(store)
|
|
12
|
+
cb = CircuitBreakerService(store)
|
|
13
|
+
engine = GovernanceEngine(store, scope_mgr, cb, mode="ENFORCE")
|
|
14
|
+
router = AigpRouter(engine, hmac_secret="...")
|
|
15
|
+
|
|
16
|
+
# Handle requests (framework-agnostic):
|
|
17
|
+
status, response = await router.handle_tool_request(headers, body)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
from .circuit_breaker import CircuitBreakerService
|
|
23
|
+
from .governance_engine import GovernanceEngine
|
|
24
|
+
from .hmac_auth import sign_request, verify_hmac
|
|
25
|
+
from .routes import AigpRouter
|
|
26
|
+
from .scope_manager import ScopeEnvelopeManager, TEMPLATES
|
|
27
|
+
from .store import GovernanceStore
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"GovernanceEngine",
|
|
31
|
+
"GovernanceStore",
|
|
32
|
+
"ScopeEnvelopeManager",
|
|
33
|
+
"CircuitBreakerService",
|
|
34
|
+
"AigpRouter",
|
|
35
|
+
"TEMPLATES",
|
|
36
|
+
"verify_hmac",
|
|
37
|
+
"sign_request",
|
|
38
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Circuit Breaker — halts anomalous agents, cascades to delegation chains."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone, timedelta
|
|
5
|
+
|
|
6
|
+
from .store import GovernanceStore
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
CLOSED, OPEN, HALF_OPEN = "CLOSED", "OPEN", "HALF_OPEN"
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONFIG = {
|
|
13
|
+
"error_rate_threshold": 0.5,
|
|
14
|
+
"time_window_seconds": 60,
|
|
15
|
+
"cooldown_seconds": 30,
|
|
16
|
+
"max_probe_requests": 3,
|
|
17
|
+
"recovery_conditions": {"consecutive_successes": 3},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _now() -> str:
|
|
22
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _expiry(seconds: int) -> str:
|
|
26
|
+
return (datetime.now(timezone.utc) + timedelta(seconds=seconds)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CircuitBreakerService:
|
|
30
|
+
def __init__(self, db: GovernanceStore):
|
|
31
|
+
self.db = db
|
|
32
|
+
|
|
33
|
+
async def is_open(self, agent_id: str) -> bool:
|
|
34
|
+
state = await self.db.get_circuit_breaker(agent_id)
|
|
35
|
+
if not state:
|
|
36
|
+
return False
|
|
37
|
+
if state.get("state") == OPEN:
|
|
38
|
+
if _now() > state.get("cooldown_until", ""):
|
|
39
|
+
await self.db.update_circuit_breaker(agent_id, {"state": HALF_OPEN})
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
async def get_state(self, agent_id: str) -> str:
|
|
45
|
+
state = await self.db.get_circuit_breaker(agent_id)
|
|
46
|
+
return state.get("state", CLOSED) if state else CLOSED
|
|
47
|
+
|
|
48
|
+
async def record_outcome(self, agent_id: str, success: bool):
|
|
49
|
+
state = await self.db.get_circuit_breaker(agent_id) or {
|
|
50
|
+
"state": CLOSED, "errors": 0, "total": 0, "consecutive_successes": 0,
|
|
51
|
+
}
|
|
52
|
+
state["total"] += 1
|
|
53
|
+
if not success:
|
|
54
|
+
state["errors"] += 1
|
|
55
|
+
state["consecutive_successes"] = 0
|
|
56
|
+
else:
|
|
57
|
+
state["consecutive_successes"] += 1
|
|
58
|
+
|
|
59
|
+
current = state.get("state", CLOSED)
|
|
60
|
+
config = await self._get_config(agent_id)
|
|
61
|
+
|
|
62
|
+
if current == CLOSED and state["total"] >= 5:
|
|
63
|
+
if state["errors"] / state["total"] > config["error_rate_threshold"]:
|
|
64
|
+
await self._trip(agent_id, config)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if current == HALF_OPEN and success:
|
|
68
|
+
if state["consecutive_successes"] >= config["recovery_conditions"]["consecutive_successes"]:
|
|
69
|
+
await self._reset(agent_id)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if current == HALF_OPEN and not success:
|
|
73
|
+
await self._trip(agent_id, config)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
await self.db.put_circuit_breaker(agent_id, state)
|
|
77
|
+
|
|
78
|
+
async def manual_reset(self, agent_id: str):
|
|
79
|
+
await self._reset(agent_id)
|
|
80
|
+
|
|
81
|
+
async def _trip(self, agent_id: str, config: dict):
|
|
82
|
+
await self.db.put_circuit_breaker(agent_id, {
|
|
83
|
+
"state": OPEN,
|
|
84
|
+
"tripped_at": _now(),
|
|
85
|
+
"cooldown_until": _expiry(config["cooldown_seconds"]),
|
|
86
|
+
"errors": 0, "total": 0, "consecutive_successes": 0,
|
|
87
|
+
})
|
|
88
|
+
logger.warning("Circuit breaker TRIPPED for agent %s", agent_id)
|
|
89
|
+
# Cascade to delegates
|
|
90
|
+
for d in await self.db.get_delegates(agent_id):
|
|
91
|
+
if target := d.get("target_agent_id"):
|
|
92
|
+
await self._trip(target, config)
|
|
93
|
+
await self.db.put_task({"type": "CIRCUIT_BREAKER", "agent_id": agent_id, "status": "PENDING"})
|
|
94
|
+
|
|
95
|
+
async def _reset(self, agent_id: str):
|
|
96
|
+
await self.db.put_circuit_breaker(agent_id, {
|
|
97
|
+
"state": CLOSED, "errors": 0, "total": 0, "consecutive_successes": 0,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
async def _get_config(self, agent_id: str) -> dict:
|
|
101
|
+
agent = await self.db.get_agent(agent_id)
|
|
102
|
+
return agent.get("circuit_breaker_config", DEFAULT_CONFIG) if agent else DEFAULT_CONFIG
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Governance Engine — evaluates AIGP agentic messages against scope envelopes."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone, timedelta
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from .circuit_breaker import CircuitBreakerService
|
|
9
|
+
from .scope_manager import ScopeEnvelopeManager, _now, _expiry
|
|
10
|
+
from .store import GovernanceStore
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GovernanceEngine:
|
|
16
|
+
"""Core AIGP decision engine. Mode: REPORT | REPORT-TRACE | ENFORCE."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, db: GovernanceStore, scope_mgr: ScopeEnvelopeManager,
|
|
19
|
+
circuit_breaker: CircuitBreakerService, mode: str = "REPORT"):
|
|
20
|
+
self.db = db
|
|
21
|
+
self.scope_mgr = scope_mgr
|
|
22
|
+
self.cb = circuit_breaker
|
|
23
|
+
self.mode = mode
|
|
24
|
+
|
|
25
|
+
async def handle_tool_request(self, msg: dict) -> dict:
|
|
26
|
+
agent_id = msg["agent_id"]
|
|
27
|
+
tool_id = msg["tool_id"]
|
|
28
|
+
|
|
29
|
+
if await self.cb.is_open(agent_id):
|
|
30
|
+
return self._decision("DENY", "CIRCUIT_OPEN", agent_id, tool_id)
|
|
31
|
+
|
|
32
|
+
envelope = await self.scope_mgr.get_active_envelope(agent_id)
|
|
33
|
+
if not envelope:
|
|
34
|
+
return self._decision("DENY", "NO_ACTIVE_SCOPE", agent_id, tool_id)
|
|
35
|
+
|
|
36
|
+
tools_perm = envelope.get("permissions", {}).get("tools", {})
|
|
37
|
+
if tool_id in tools_perm.get("denied", []):
|
|
38
|
+
return self._decision("DENY", "TOOL_DENIED", agent_id, tool_id)
|
|
39
|
+
if tools_perm.get("mode") == "allowlist" and tool_id not in tools_perm.get("allowed", []):
|
|
40
|
+
return self._decision("DENY", "TOOL_NOT_IN_ALLOWLIST", agent_id, tool_id)
|
|
41
|
+
|
|
42
|
+
budget = await self._check_budget(agent_id, envelope)
|
|
43
|
+
if not budget["ok"]:
|
|
44
|
+
return self._decision("DENY", f"BUDGET_DEPLETED:{budget['reason']}", agent_id, tool_id)
|
|
45
|
+
|
|
46
|
+
await self.db.increment_budget(agent_id, actions=1)
|
|
47
|
+
return self._decision("ALLOW", None, agent_id, tool_id, budget_remaining=budget["remaining"])
|
|
48
|
+
|
|
49
|
+
async def handle_plan_submit(self, msg: dict) -> dict:
|
|
50
|
+
agent_id = msg["agent_id"]
|
|
51
|
+
envelope = await self.scope_mgr.get_active_envelope(agent_id)
|
|
52
|
+
if not envelope:
|
|
53
|
+
return {"decision": "REJECTED", "plan_id": msg.get("plan_id"), "reason": "NO_ACTIVE_SCOPE"}
|
|
54
|
+
|
|
55
|
+
allowed_tools = set(envelope.get("permissions", {}).get("tools", {}).get("allowed", []))
|
|
56
|
+
modifications = []
|
|
57
|
+
for step in msg.get("steps", []):
|
|
58
|
+
tool_id = step.get("tool_id", "")
|
|
59
|
+
if tool_id and tool_id not in allowed_tools:
|
|
60
|
+
return {"decision": "REJECTED", "plan_id": msg["plan_id"], "reason": f"Tool '{tool_id}' not in scope"}
|
|
61
|
+
for gate in envelope.get("approval_gates", []):
|
|
62
|
+
if fnmatch.fnmatch(tool_id, gate.get("action_pattern", "")):
|
|
63
|
+
modifications.append({"step_number": step["step_number"], "gate": gate["requires"], "reason": f"Matches: {gate['action_pattern']}"})
|
|
64
|
+
|
|
65
|
+
if modifications:
|
|
66
|
+
return {"decision": "APPROVED_WITH_MODIFICATIONS", "plan_id": msg["plan_id"], "modifications": modifications}
|
|
67
|
+
return {"decision": "APPROVED", "plan_id": msg["plan_id"]}
|
|
68
|
+
|
|
69
|
+
async def handle_step_complete(self, msg: dict) -> dict:
|
|
70
|
+
agent_id = msg["agent_id"]
|
|
71
|
+
await self.db.increment_budget(agent_id, tokens=msg.get("tokens_used", 0))
|
|
72
|
+
await self.cb.record_outcome(agent_id, msg.get("status") == "SUCCESS")
|
|
73
|
+
run_id = msg.get("plan_id", msg.get("run_id", ""))
|
|
74
|
+
if run_id:
|
|
75
|
+
await self.db.put_run_step(run_id, {"step_number": msg.get("step_number", 0), "status": msg.get("status", ""), "tokens_used": msg.get("tokens_used", 0)})
|
|
76
|
+
return {"status": "recorded"}
|
|
77
|
+
|
|
78
|
+
async def handle_escalate(self, msg: dict) -> dict:
|
|
79
|
+
task_id = await self.db.put_task({
|
|
80
|
+
"type": "ESCALATION", "agent_id": msg["agent_id"],
|
|
81
|
+
"escalation_id": msg.get("escalation_id", f"esc-{uuid4().hex[:8]}"),
|
|
82
|
+
"reason": msg.get("reason", ""), "action": msg.get("action", {}),
|
|
83
|
+
"context": msg.get("context", {}),
|
|
84
|
+
"timeout_seconds": msg.get("timeout_seconds", 300),
|
|
85
|
+
"timeout_action": msg.get("timeout_action", "DENY"), "status": "PENDING",
|
|
86
|
+
})
|
|
87
|
+
return {"status": "PAUSED", "task_id": task_id}
|
|
88
|
+
|
|
89
|
+
async def handle_delegate(self, msg: dict) -> dict:
|
|
90
|
+
parent_id = msg["delegating_agent_id"]
|
|
91
|
+
target_id = msg["target_agent_id"]
|
|
92
|
+
chain = msg.get("delegation_chain", [])
|
|
93
|
+
|
|
94
|
+
if len(chain) >= 5:
|
|
95
|
+
return {"decision": "DENIED", "reason": "MAX_DELEGATION_DEPTH_EXCEEDED"}
|
|
96
|
+
|
|
97
|
+
parent_scope = await self.scope_mgr.get_active_envelope(parent_id)
|
|
98
|
+
if not parent_scope:
|
|
99
|
+
return {"decision": "DENIED", "reason": "PARENT_NO_ACTIVE_SCOPE"}
|
|
100
|
+
|
|
101
|
+
requested = msg.get("requested_scope", {})
|
|
102
|
+
narrowed = self._intersect_scope(parent_scope, requested)
|
|
103
|
+
ttl = narrowed["budgets"].get("max_ttl_seconds", 1800)
|
|
104
|
+
|
|
105
|
+
await self.db.put_scope_envelope({
|
|
106
|
+
"scope_envelope_id": f"del-{uuid4().hex[:12]}",
|
|
107
|
+
"agent_id": target_id, "version": 1, "status": "ACTIVE",
|
|
108
|
+
"permissions": narrowed["permissions"], "budgets": narrowed["budgets"],
|
|
109
|
+
"autonomy_level": parent_scope.get("autonomy_level", "ACT_WITH_APPROVAL"),
|
|
110
|
+
"delegation_chain": chain + [parent_id],
|
|
111
|
+
"issued_at": _now(), "expires_at": _expiry(ttl),
|
|
112
|
+
})
|
|
113
|
+
await self.db.put_delegation({"delegating_agent_id": parent_id, "target_agent_id": target_id, "delegation_chain": chain + [parent_id]})
|
|
114
|
+
return {"decision": "ALLOWED", "target_agent_id": target_id, "narrowed_scope": narrowed}
|
|
115
|
+
|
|
116
|
+
async def handle_memory_write(self, msg: dict) -> dict:
|
|
117
|
+
agent_id = msg["agent_id"]
|
|
118
|
+
envelope = await self.scope_mgr.get_active_envelope(agent_id)
|
|
119
|
+
if not envelope:
|
|
120
|
+
return {"decision": "DENY", "reason": "NO_ACTIVE_SCOPE"}
|
|
121
|
+
|
|
122
|
+
mem_scope = envelope.get("memory_scope", {})
|
|
123
|
+
if not mem_scope.get("may_write", False):
|
|
124
|
+
return {"decision": "DENY", "reason": "MEMORY_WRITE_NOT_PERMITTED"}
|
|
125
|
+
|
|
126
|
+
classification = msg.get("data_classification", "HIGH")
|
|
127
|
+
allowed = mem_scope.get("allowed_classifications", [])
|
|
128
|
+
if allowed and classification not in allowed:
|
|
129
|
+
return {"decision": "DENY", "reason": f"CLASSIFICATION_{classification}_NOT_ALLOWED"}
|
|
130
|
+
|
|
131
|
+
max_ret = mem_scope.get("max_retention_seconds", 86400)
|
|
132
|
+
return {"decision": "ALLOW", "actual_retention_seconds": min(msg.get("retention_seconds", 86400), max_ret), "isolation": mem_scope.get("isolation", "AGENT_PRIVATE")}
|
|
133
|
+
|
|
134
|
+
def _decision(self, decision: str, reason: str | None, agent_id: str, tool_id: str, **extra) -> dict:
|
|
135
|
+
result = {"decision": decision, "reason": reason, "agent_id": agent_id, "tool_id": tool_id, **extra}
|
|
136
|
+
if self.mode in ("REPORT", "REPORT-TRACE") and decision == "DENY":
|
|
137
|
+
result["decision"] = "ALLOW"
|
|
138
|
+
result["report_only_denial"] = True
|
|
139
|
+
result["original_decision"] = "DENY"
|
|
140
|
+
result["original_reason"] = reason
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def _intersect_scope(self, parent: dict, requested: dict) -> dict:
|
|
144
|
+
parent_tools = set(parent.get("permissions", {}).get("tools", {}).get("allowed", []))
|
|
145
|
+
req_tools = set(requested.get("tools", []))
|
|
146
|
+
narrowed_tools = list(parent_tools & req_tools) if req_tools else list(parent_tools)
|
|
147
|
+
parent_budgets = parent.get("budgets", {})
|
|
148
|
+
return {
|
|
149
|
+
"permissions": {"tools": {"mode": "allowlist", "allowed": narrowed_tools}},
|
|
150
|
+
"budgets": {
|
|
151
|
+
"max_actions": min(requested.get("max_actions", 999), parent_budgets.get("max_actions", 999)),
|
|
152
|
+
"max_tokens": min(requested.get("max_tokens", 999999), parent_budgets.get("max_tokens", 999999)),
|
|
153
|
+
"max_cost_usd": min(requested.get("max_cost_usd", 99), parent_budgets.get("max_cost_usd", 99)),
|
|
154
|
+
"max_ttl_seconds": min(requested.get("max_ttl_seconds", 3600), parent_budgets.get("max_ttl_seconds", 3600)),
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async def _check_budget(self, agent_id: str, envelope: dict) -> dict:
|
|
159
|
+
remaining = await self.db.get_budget_remaining(agent_id)
|
|
160
|
+
budgets = envelope.get("budgets", {})
|
|
161
|
+
if remaining.get("actions", 0) >= budgets.get("max_actions", 999):
|
|
162
|
+
return {"ok": False, "reason": "actions", "remaining": remaining}
|
|
163
|
+
if remaining.get("tokens", 0) >= budgets.get("max_tokens", 999999):
|
|
164
|
+
return {"ok": False, "reason": "tokens", "remaining": remaining}
|
|
165
|
+
return {"ok": True, "remaining": remaining}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""HMAC-SHA256 authentication for AIGP protocol messages."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
MAX_TIMESTAMP_DRIFT_SECONDS = 300
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def verify_hmac(headers: dict, body: bytes, secret: str) -> bool:
|
|
14
|
+
"""Verify AIGP HMAC-SHA256 signature.
|
|
15
|
+
|
|
16
|
+
Headers (case-insensitive):
|
|
17
|
+
X-AIGP-Signature: hmac-sha256={hex_digest}
|
|
18
|
+
X-AIGP-Timestamp: ISO 8601 timestamp
|
|
19
|
+
X-AIGP-Agent-Id: agent identifier
|
|
20
|
+
"""
|
|
21
|
+
sig_header = headers.get("x-aigp-signature", "")
|
|
22
|
+
timestamp = headers.get("x-aigp-timestamp", "")
|
|
23
|
+
|
|
24
|
+
if not sig_header or not timestamp:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
ts = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
29
|
+
if abs((datetime.now(timezone.utc) - ts).total_seconds()) > MAX_TIMESTAMP_DRIFT_SECONDS:
|
|
30
|
+
return False
|
|
31
|
+
except (ValueError, TypeError):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
if not sig_header.startswith("hmac-sha256="):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
body_hash = hashlib.sha256(body).hexdigest()
|
|
38
|
+
message = f"{timestamp}\n{body_hash}"
|
|
39
|
+
expected = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
|
|
40
|
+
return hmac.compare_digest(sig_header[len("hmac-sha256="):], expected)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sign_request(body: bytes, secret: str, agent_id: str) -> dict:
|
|
44
|
+
"""Generate AIGP HMAC headers for outbound requests."""
|
|
45
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
46
|
+
body_hash = hashlib.sha256(body).hexdigest()
|
|
47
|
+
message = f"{timestamp}\n{body_hash}"
|
|
48
|
+
sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
|
|
49
|
+
return {
|
|
50
|
+
"X-AIGP-Signature": f"hmac-sha256={sig}",
|
|
51
|
+
"X-AIGP-Timestamp": timestamp,
|
|
52
|
+
"X-AIGP-Agent-Id": agent_id,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""AIGP Server Routes — framework-agnostic handler functions.
|
|
2
|
+
|
|
3
|
+
These return (status_code, response_dict) tuples. The host application
|
|
4
|
+
(GOV_APP, RUN_PRJ) wraps them in its framework's response type.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .governance_engine import GovernanceEngine
|
|
11
|
+
from .hmac_auth import verify_hmac
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AigpRouter:
|
|
17
|
+
"""Stateless router that delegates to GovernanceEngine."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, engine: GovernanceEngine, hmac_secret: str = ""):
|
|
20
|
+
self.engine = engine
|
|
21
|
+
self.secret = hmac_secret
|
|
22
|
+
|
|
23
|
+
def _verify(self, headers: dict, body: bytes) -> dict | None:
|
|
24
|
+
"""Verify HMAC and parse body. Returns parsed dict or None."""
|
|
25
|
+
if self.secret and not verify_hmac(headers, body, self.secret):
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
return json.loads(body)
|
|
29
|
+
except (json.JSONDecodeError, ValueError):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
async def handle_tool_request(self, headers: dict, body: bytes) -> tuple[int, dict]:
|
|
33
|
+
msg = self._verify(headers, body)
|
|
34
|
+
if msg is None:
|
|
35
|
+
return 401, {"error": "unauthorized"}
|
|
36
|
+
result = await self.engine.handle_tool_request(msg)
|
|
37
|
+
await self.engine.db.put_audit(msg.get("agent_id", ""), "TOOL_REQUEST", result["decision"], {"tool_id": msg.get("tool_id"), "reason": result.get("reason")})
|
|
38
|
+
return 200, result
|
|
39
|
+
|
|
40
|
+
async def handle_plan_submit(self, headers: dict, body: bytes) -> tuple[int, dict]:
|
|
41
|
+
msg = self._verify(headers, body)
|
|
42
|
+
if msg is None:
|
|
43
|
+
return 401, {"error": "unauthorized"}
|
|
44
|
+
result = await self.engine.handle_plan_submit(msg)
|
|
45
|
+
await self.engine.db.put_audit(msg.get("agent_id", ""), "PLAN_SUBMIT", result["decision"], msg)
|
|
46
|
+
return 200, result
|
|
47
|
+
|
|
48
|
+
async def handle_step_complete(self, headers: dict, body: bytes) -> tuple[int, dict]:
|
|
49
|
+
msg = self._verify(headers, body)
|
|
50
|
+
if msg is None:
|
|
51
|
+
return 401, {"error": "unauthorized"}
|
|
52
|
+
result = await self.engine.handle_step_complete(msg)
|
|
53
|
+
await self.engine.db.put_audit(msg.get("agent_id", ""), "STEP_COMPLETE", msg.get("status", ""), msg)
|
|
54
|
+
return 200, result
|
|
55
|
+
|
|
56
|
+
async def handle_escalate(self, headers: dict, body: bytes) -> tuple[int, dict]:
|
|
57
|
+
msg = self._verify(headers, body)
|
|
58
|
+
if msg is None:
|
|
59
|
+
return 401, {"error": "unauthorized"}
|
|
60
|
+
result = await self.engine.handle_escalate(msg)
|
|
61
|
+
await self.engine.db.put_audit(msg.get("agent_id", ""), "ESCALATE", result.get("status", ""), msg)
|
|
62
|
+
return 200, result
|
|
63
|
+
|
|
64
|
+
async def handle_delegate(self, headers: dict, body: bytes) -> tuple[int, dict]:
|
|
65
|
+
msg = self._verify(headers, body)
|
|
66
|
+
if msg is None:
|
|
67
|
+
return 401, {"error": "unauthorized"}
|
|
68
|
+
result = await self.engine.handle_delegate(msg)
|
|
69
|
+
await self.engine.db.put_audit(msg.get("delegating_agent_id", ""), "DELEGATE", result["decision"], msg)
|
|
70
|
+
return 200, result
|
|
71
|
+
|
|
72
|
+
async def handle_memory_write(self, headers: dict, body: bytes) -> tuple[int, dict]:
|
|
73
|
+
msg = self._verify(headers, body)
|
|
74
|
+
if msg is None:
|
|
75
|
+
return 401, {"error": "unauthorized"}
|
|
76
|
+
result = await self.engine.handle_memory_write(msg)
|
|
77
|
+
await self.engine.db.put_audit(msg.get("agent_id", ""), "MEMORY_WRITE", result["decision"], msg)
|
|
78
|
+
return 200, result
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Scope Envelope Manager — create, approve, enforce scope envelopes with SoD."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone, timedelta
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from .store import GovernanceStore
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
TEMPLATES = {
|
|
13
|
+
"ANALYST": {
|
|
14
|
+
"permissions": {"tools": {"mode": "allowlist", "allowed": []}, "models": {"mode": "allowlist", "allowed": ["us.anthropic.claude-sonnet-4-5-20250929-v1:0"]}},
|
|
15
|
+
"budgets": {"max_actions": 100, "max_tokens": 200000, "max_cost_usd": 10.0, "max_ttl_seconds": 3600},
|
|
16
|
+
"autonomy_level": "ACT_AUTONOMOUS",
|
|
17
|
+
"memory_scope": {"may_write": False},
|
|
18
|
+
},
|
|
19
|
+
"EXECUTOR": {
|
|
20
|
+
"permissions": {"tools": {"mode": "allowlist", "allowed": []}, "models": {"mode": "allowlist", "allowed": ["us.anthropic.claude-sonnet-4-5-20250929-v1:0"]}},
|
|
21
|
+
"budgets": {"max_actions": 50, "max_tokens": 100000, "max_cost_usd": 5.0, "max_ttl_seconds": 3600},
|
|
22
|
+
"autonomy_level": "ACT_WITH_APPROVAL",
|
|
23
|
+
"approval_gates": [{"action_pattern": "*_delete_*", "requires": "HUMAN_APPROVAL", "timeout_seconds": 300, "timeout_action": "DENY"}],
|
|
24
|
+
"memory_scope": {"may_write": True, "max_retention_seconds": 86400, "allowed_classifications": ["LOW", "MEDIUM"], "isolation": "AGENT_PRIVATE"},
|
|
25
|
+
},
|
|
26
|
+
"ORCHESTRATOR": {
|
|
27
|
+
"permissions": {"tools": {"mode": "allowlist", "allowed": []}, "models": {"mode": "allowlist", "allowed": ["us.anthropic.claude-opus-4-7"]}},
|
|
28
|
+
"budgets": {"max_actions": 200, "max_tokens": 500000, "max_cost_usd": 25.0, "max_ttl_seconds": 3600},
|
|
29
|
+
"autonomy_level": "ACT_WITH_APPROVAL",
|
|
30
|
+
"memory_scope": {"may_write": True, "max_retention_seconds": 3600, "allowed_classifications": ["LOW", "MEDIUM", "HIGH"], "isolation": "APP_SHARED"},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _expiry(ttl_seconds: int) -> str:
|
|
40
|
+
return (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ScopeEnvelopeManager:
|
|
44
|
+
def __init__(self, db: GovernanceStore):
|
|
45
|
+
self.db = db
|
|
46
|
+
|
|
47
|
+
async def create_request(self, agent_id: str, envelope: dict, requester: str) -> str:
|
|
48
|
+
envelope_id = f"scope-{uuid4().hex[:12]}"
|
|
49
|
+
version = len(await self.db.query_scopes(agent_id)) + 1
|
|
50
|
+
envelope.update({
|
|
51
|
+
"scope_envelope_id": envelope_id,
|
|
52
|
+
"agent_id": agent_id,
|
|
53
|
+
"version": version,
|
|
54
|
+
"status": "PENDING_APPROVAL",
|
|
55
|
+
"created_by": requester,
|
|
56
|
+
"created_at": _now(),
|
|
57
|
+
})
|
|
58
|
+
await self.db.put_scope_envelope(envelope)
|
|
59
|
+
task_id = await self.db.put_task({
|
|
60
|
+
"type": "SCOPE_APPROVAL", "agent_id": agent_id,
|
|
61
|
+
"envelope_id": envelope_id, "requester": requester, "status": "PENDING",
|
|
62
|
+
})
|
|
63
|
+
return task_id
|
|
64
|
+
|
|
65
|
+
async def approve(self, envelope_id: str, approver: str) -> bool:
|
|
66
|
+
envelope = await self.db.get_scope_envelope(envelope_id)
|
|
67
|
+
if not envelope:
|
|
68
|
+
raise ValueError("Envelope not found")
|
|
69
|
+
if envelope["created_by"] == approver:
|
|
70
|
+
raise PermissionError("SoD violation: approver cannot be the creator")
|
|
71
|
+
ttl = envelope.get("budgets", {}).get("max_ttl_seconds", 3600)
|
|
72
|
+
await self.db.update_scope_envelope(envelope_id, {
|
|
73
|
+
"status": "ACTIVE", "approved_by": approver,
|
|
74
|
+
"issued_at": _now(), "expires_at": _expiry(ttl),
|
|
75
|
+
})
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
async def reject(self, envelope_id: str, rejector: str, reason: str = "") -> bool:
|
|
79
|
+
await self.db.update_scope_envelope(envelope_id, {
|
|
80
|
+
"status": "REJECTED", "rejected_by": rejector,
|
|
81
|
+
"rejection_reason": reason, "rejected_at": _now(),
|
|
82
|
+
})
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
async def get_active_envelope(self, agent_id: str) -> dict | None:
|
|
86
|
+
envelope = await self.db.get_active_scope(agent_id)
|
|
87
|
+
if not envelope:
|
|
88
|
+
return None
|
|
89
|
+
if envelope.get("expires_at", "") and envelope["expires_at"] < _now():
|
|
90
|
+
await self.db.update_scope_envelope(envelope["scope_envelope_id"], {"status": "EXPIRED"})
|
|
91
|
+
return None
|
|
92
|
+
return envelope
|
|
93
|
+
|
|
94
|
+
async def from_template(self, template_name: str, agent_id: str, tools: list[str] | None = None) -> dict:
|
|
95
|
+
envelope = copy.deepcopy(TEMPLATES.get(template_name, TEMPLATES["EXECUTOR"]))
|
|
96
|
+
if tools:
|
|
97
|
+
envelope["permissions"]["tools"]["allowed"] = tools
|
|
98
|
+
envelope["agent_id"] = agent_id
|
|
99
|
+
return envelope
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Storage protocol — abstract interface that host applications must implement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GovernanceStore(ABC):
|
|
9
|
+
"""Abstract storage backend for the AIGP governance server.
|
|
10
|
+
|
|
11
|
+
Host applications (GOV_APP, RUN_PRJ) implement this interface
|
|
12
|
+
to provide persistence for scope envelopes, circuit breakers,
|
|
13
|
+
budgets, delegations, audits, and tasks.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# --- Scope Envelopes ---
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def put_scope_envelope(self, envelope: dict) -> None: ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def get_scope_envelope(self, envelope_id: str) -> dict | None: ...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def get_active_scope(self, agent_id: str) -> dict | None: ...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def update_scope_envelope(self, envelope_id: str, updates: dict) -> None: ...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def query_scopes(self, agent_id: str) -> list[dict]: ...
|
|
32
|
+
|
|
33
|
+
# --- Circuit Breaker ---
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def get_circuit_breaker(self, agent_id: str) -> dict | None: ...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def put_circuit_breaker(self, agent_id: str, state: dict) -> None: ...
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def update_circuit_breaker(self, agent_id: str, updates: dict) -> None: ...
|
|
43
|
+
|
|
44
|
+
# --- Budget ---
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def increment_budget(self, agent_id: str, actions: int = 0, tokens: int = 0) -> None: ...
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def get_budget_remaining(self, agent_id: str) -> dict: ...
|
|
51
|
+
|
|
52
|
+
# --- Delegation ---
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def put_delegation(self, delegation: dict) -> None: ...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def get_delegates(self, agent_id: str) -> list[dict]: ...
|
|
59
|
+
|
|
60
|
+
# --- Agent ---
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def get_agent(self, agent_id: str) -> dict | None: ...
|
|
64
|
+
|
|
65
|
+
# --- Tasks (escalations, approvals) ---
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
async def put_task(self, task: dict) -> str: ...
|
|
69
|
+
|
|
70
|
+
# --- Audit ---
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def put_audit(self, agent_id: str, action: str, decision: str, details: dict) -> None: ...
|
|
74
|
+
|
|
75
|
+
# --- Run Steps ---
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def put_run_step(self, run_id: str, step: dict) -> None: ...
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aigp-server"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AIGP Governance Server — agentic governance engine (scope envelopes, circuit breakers, delegation)"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = []
|
|
11
|
+
license = {text = "Proprietary"}
|
|
12
|
+
authors = [{name = "Evan Erwee", email = "evan@erwee.com"}]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
keywords = ["ai", "governance", "protocol", "aigp", "agentic", "scope-envelope", "circuit-breaker"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: Other/Proprietary License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/owner-spec/aigp-protocol"
|
|
29
|
+
Repository = "https://github.com/owner-spec/aigp-protocol"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest", "pytest-asyncio"]
|