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.
@@ -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"]