maigp-client 5.0.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.
Files changed (49) hide show
  1. maigp_client-5.0.0/.gitignore +45 -0
  2. maigp_client-5.0.0/Makefile +13 -0
  3. maigp_client-5.0.0/PKG-INFO +178 -0
  4. maigp_client-5.0.0/README.md +151 -0
  5. maigp_client-5.0.0/aigp_client/__init__.py +64 -0
  6. maigp_client-5.0.0/aigp_client/african_context.py +102 -0
  7. maigp_client-5.0.0/aigp_client/agent_client.py +619 -0
  8. maigp_client-5.0.0/aigp_client/anticipation.py +72 -0
  9. maigp_client-5.0.0/aigp_client/backoff.py +32 -0
  10. maigp_client-5.0.0/aigp_client/client.py +658 -0
  11. maigp_client-5.0.0/aigp_client/config.py +62 -0
  12. maigp_client-5.0.0/aigp_client/context.py +112 -0
  13. maigp_client-5.0.0/aigp_client/delegation.py +66 -0
  14. maigp_client-5.0.0/aigp_client/govern.py +25 -0
  15. maigp_client-5.0.0/aigp_client/japanese_context.py +83 -0
  16. maigp_client-5.0.0/aigp_client/logger.py +35 -0
  17. maigp_client-5.0.0/aigp_client/providers/__init__.py +10 -0
  18. maigp_client-5.0.0/aigp_client/providers/consent/__init__.py +6 -0
  19. maigp_client-5.0.0/aigp_client/providers/consent/consent_helper_base.py +31 -0
  20. maigp_client-5.0.0/aigp_client/providers/consent/default/__init__.py +0 -0
  21. maigp_client-5.0.0/aigp_client/providers/consent/default/passthrough_helper.py +13 -0
  22. maigp_client-5.0.0/aigp_client/providers/model/__init__.py +7 -0
  23. maigp_client-5.0.0/aigp_client/providers/model/azure_openai/__init__.py +0 -0
  24. maigp_client-5.0.0/aigp_client/providers/model/bedrock/__init__.py +0 -0
  25. maigp_client-5.0.0/aigp_client/providers/model/bedrock/bedrock_model_provider.py +75 -0
  26. maigp_client-5.0.0/aigp_client/providers/model/model_provider_base.py +38 -0
  27. maigp_client-5.0.0/aigp_client/providers/model/model_provider_factory.py +24 -0
  28. maigp_client-5.0.0/aigp_client/providers/model/vertex/__init__.py +0 -0
  29. maigp_client-5.0.0/aigp_client/providers/storage/__init__.py +5 -0
  30. maigp_client-5.0.0/aigp_client/providers/storage/local/__init__.py +5 -0
  31. maigp_client-5.0.0/aigp_client/providers/storage/local/local_storage_provider.py +54 -0
  32. maigp_client-5.0.0/aigp_client/providers/storage/s3/__init__.py +5 -0
  33. maigp_client-5.0.0/aigp_client/providers/storage/s3/s3_storage_provider.py +56 -0
  34. maigp_client-5.0.0/aigp_client/providers/storage/storage_provider_base.py +32 -0
  35. maigp_client-5.0.0/aigp_client/providers/transport/__init__.py +6 -0
  36. maigp_client-5.0.0/aigp_client/providers/transport/httpx/__init__.py +0 -0
  37. maigp_client-5.0.0/aigp_client/providers/transport/httpx/httpx_transport.py +29 -0
  38. maigp_client-5.0.0/aigp_client/providers/transport/transport_base.py +24 -0
  39. maigp_client-5.0.0/aigp_client/regulatory.py +81 -0
  40. maigp_client-5.0.0/aigp_client/schemas/__init__.py +5 -0
  41. maigp_client-5.0.0/aigp_client/schemas/geo.py +106 -0
  42. maigp_client-5.0.0/aigp_client/session.py +28 -0
  43. maigp_client-5.0.0/aigp_client/streaming.py +71 -0
  44. maigp_client-5.0.0/aigp_client/tokens.py +46 -0
  45. maigp_client-5.0.0/aigp_client/tools.py +50 -0
  46. maigp_client-5.0.0/aigp_client/trace_builder.py +140 -0
  47. maigp_client-5.0.0/pyproject.toml +36 -0
  48. maigp_client-5.0.0/tests/test_agent_client_orchestration.py +346 -0
  49. maigp_client-5.0.0/tests/test_agent_client_safety.py +249 -0
@@ -0,0 +1,45 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .eggs/
9
+
10
+ # Node
11
+ node_modules/
12
+ dist/
13
+
14
+ # Go
15
+ bin/
16
+
17
+ # Rust
18
+ target/
19
+
20
+ # .NET
21
+ bin/
22
+ obj/
23
+
24
+ # Java
25
+ *.class
26
+ out/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ .kiro/
32
+ *.swp
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Secrets (never commit)
39
+ .env
40
+ *.pem
41
+ *.key
42
+ *.pdf
43
+ BACKLOG/
44
+ patent/
45
+ docs/.archive/
@@ -0,0 +1,13 @@
1
+ .PHONY: wheel clean install test
2
+
3
+ wheel:
4
+ python3 -m build --wheel --outdir dist/
5
+
6
+ clean:
7
+ rm -rf dist/ build/ *.egg-info aigp_client/__pycache__
8
+
9
+ install:
10
+ pip install dist/aigp_client-1.0.0-py3-none-any.whl
11
+
12
+ test:
13
+ pytest tests/ -v
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: maigp-client
3
+ Version: 5.0.0
4
+ Summary: MAIGP (Mediated AI Governance Protocol) client — consent-based runtime AI governance
5
+ Project-URL: Homepage, https://github.com/owner-spec/aigp-protocol
6
+ Project-URL: Documentation, https://github.com/owner-spec/aigp-protocol/tree/main/specification
7
+ Project-URL: Repository, https://github.com/owner-spec/aigp-protocol
8
+ Author-email: Evan Erwee <evan@erwee.com>
9
+ License: Proprietary
10
+ Keywords: ai,aigp,bedrock,governance,llm,maigp,mediated,protocol
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: httpx>=0.25.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio; extra == 'dev'
25
+ Requires-Dist: respx; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # aigp-client
29
+
30
+ Universal AI Governance Protocol (AIGP) client v4.0 — consent-based runtime AI governance with jurisdictional enforcement.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install aigp-client
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ from aigp_client import AigpClient
42
+
43
+ client = AigpClient("https://your-gov-server.com", "my-app", "hmac-secret")
44
+
45
+ # Declare governance context (once at startup)
46
+ client.declare_universal_context({"active": True, "principles": "ALL"})
47
+
48
+ # Every AI call: CHECK → invoke → RECORD
49
+ decision = await client.check("chat", "claude-4")
50
+ # ... invoke your model ...
51
+ await client.record(use_case="chat", model_id="claude-4", input_tokens=500, output_tokens=200, duration_ms=1200)
52
+ ```
53
+
54
+ ## One-Line Governance
55
+
56
+ ```python
57
+ from aigp_client import govern
58
+
59
+ aigp = govern("https://gov-server.com", "my-app", "secret")
60
+ result = await aigp.wrap(my_ai_function) # CHECK + RECORD automatic
61
+ ```
62
+
63
+ ## Jurisdictional Governance (v4.0)
64
+
65
+ Declare which legal frameworks govern your AI system:
66
+
67
+ ```python
68
+ from aigp_client import (
69
+ AigpClient, HumanitarianContext, RegulatoryContext,
70
+ AfricanContext, JapaneseContext,
71
+ )
72
+
73
+ client = AigpClient("https://gov.example.com", "my-app", "secret")
74
+
75
+ # Universal baseline (RFC-031) — all apps should declare this
76
+ client.declare_universal_context({"active": True, "principles": "ALL", "accountability_required": True})
77
+
78
+ # EU AI Act (RFC-028)
79
+ client.declare_regulatory_context(RegulatoryContext(
80
+ active=True, framework="EU_AI_ACT", risk_classification="HIGH_RISK",
81
+ deployer_jurisdiction="DE",
82
+ ))
83
+
84
+ # Japan AI Promotion Act (RFC-030)
85
+ client.declare_japanese_context(JapaneseContext(
86
+ active=True, governance_framework="JAPAN_AI_PROMOTION_ACT",
87
+ sector="HEALTHCARE", innovation_priority=True,
88
+ ))
89
+
90
+ # AU Continental AI Strategy (RFC-029)
91
+ client.declare_african_context(AfricanContext(
92
+ active=True, governance_framework="AU_CONTINENTAL_AI_STRATEGY",
93
+ member_state="KE", ubuntu_principle=True,
94
+ ))
95
+
96
+ # IHL — conflict zones (RFC-027)
97
+ client.declare_humanitarian_context(HumanitarianContext(
98
+ active=True, legal_regime="IHL_GCIV", conflict_classification="NIAC",
99
+ operating_authority="ICRC",
100
+ ))
101
+ ```
102
+
103
+ All subsequent CHECK requests are evaluated against declared contexts. Priority: IHL → EU → AU → Japan → Universal.
104
+
105
+ ## User Feedback (RFC-026)
106
+
107
+ ```python
108
+ await client.feedback(request_id="req-123", session_id="ses-abc",
109
+ rating="thumbs_up", pseudonym_id="user-hash")
110
+ ```
111
+
112
+ ## Multi-Agent Delegation
113
+
114
+ ```python
115
+ from aigp_client import SessionContext, DelegationToken
116
+
117
+ session = SessionContext(user_id="coordinator@company.com")
118
+ child = session.child(scope=["search_only"])
119
+
120
+ token = DelegationToken(issuer="coordinator", delegate="worker",
121
+ allowed_models=["claude-4"], max_tokens=10000)
122
+ encoded = token.encode("shared-secret")
123
+ ```
124
+
125
+ ## Token Tracking
126
+
127
+ ```python
128
+ from aigp_client import TokenAccumulator
129
+
130
+ tokens = TokenAccumulator()
131
+ tokens.add_from_usage({"prompt_tokens": 500, "completion_tokens": 200}) # OpenAI
132
+ tokens.add_from_usage({"inputTokens": 300, "outputTokens": 150}) # Bedrock
133
+ print(f"Total: {tokens.total_input} in, {tokens.total_output} out")
134
+ ```
135
+
136
+ ## Full Lifecycle Trace
137
+
138
+ ```python
139
+ from aigp_client import TraceBuilder
140
+
141
+ tb = TraceBuilder(request_id="req-1")
142
+ tb.start(1); tb.end(1, attributes={"user_id": "user@co.com"}) # Identity
143
+ tb.start(3); tb.end(3, attributes={"decision": "ALLOW"}) # CHECK
144
+ tb.start(9); tb.end(9, attributes={"model": "claude-4"}) # Invoke
145
+ tb.add_tool_span("search_docs", duration_ms=45) # Tool
146
+ tb.start(14); tb.end(14) # RECORD
147
+ spans, summary = tb.build()
148
+ await client.trace(trace_id=tb.trace_id, spans=spans, summary=summary)
149
+ ```
150
+
151
+ ## Tool Governance
152
+
153
+ ```python
154
+ from aigp_client import ToolGovernance
155
+
156
+ tg = ToolGovernance(client=client)
157
+ decision = await tg.check_tool("execute_sql", params={"query": "SELECT *"})
158
+ ```
159
+
160
+ ## Modes
161
+
162
+ | Mode | Behavior | When server unreachable |
163
+ |------|----------|:-:|
164
+ | `REPORT` | Log all, allow all | Allow (fail-open) |
165
+ | `REPORT-TRACE` | Log + stage-level spans | Allow (fail-open) |
166
+ | `ENFORCE` | Check policies, block violations | Deny (fail-closed) |
167
+
168
+ ## Supported Languages
169
+
170
+ | Python | TypeScript | Go | Java | C# | Rust | Ruby | Swift |
171
+ |:------:|:----------:|:--:|:----:|:--:|:----:|:----:|:-----:|
172
+ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
173
+
174
+ All 8 SDKs have: `declareHumanitarianContext`, `declareRegulatoryContext`, `declareAfricanContext`, `declareJapaneseContext`, `declareUniversalContext`.
175
+
176
+ ## License
177
+
178
+ Proprietary — © 2025-2026 Evan Erwee. All rights reserved.
@@ -0,0 +1,151 @@
1
+ # aigp-client
2
+
3
+ Universal AI Governance Protocol (AIGP) client v4.0 — consent-based runtime AI governance with jurisdictional enforcement.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install aigp-client
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from aigp_client import AigpClient
15
+
16
+ client = AigpClient("https://your-gov-server.com", "my-app", "hmac-secret")
17
+
18
+ # Declare governance context (once at startup)
19
+ client.declare_universal_context({"active": True, "principles": "ALL"})
20
+
21
+ # Every AI call: CHECK → invoke → RECORD
22
+ decision = await client.check("chat", "claude-4")
23
+ # ... invoke your model ...
24
+ await client.record(use_case="chat", model_id="claude-4", input_tokens=500, output_tokens=200, duration_ms=1200)
25
+ ```
26
+
27
+ ## One-Line Governance
28
+
29
+ ```python
30
+ from aigp_client import govern
31
+
32
+ aigp = govern("https://gov-server.com", "my-app", "secret")
33
+ result = await aigp.wrap(my_ai_function) # CHECK + RECORD automatic
34
+ ```
35
+
36
+ ## Jurisdictional Governance (v4.0)
37
+
38
+ Declare which legal frameworks govern your AI system:
39
+
40
+ ```python
41
+ from aigp_client import (
42
+ AigpClient, HumanitarianContext, RegulatoryContext,
43
+ AfricanContext, JapaneseContext,
44
+ )
45
+
46
+ client = AigpClient("https://gov.example.com", "my-app", "secret")
47
+
48
+ # Universal baseline (RFC-031) — all apps should declare this
49
+ client.declare_universal_context({"active": True, "principles": "ALL", "accountability_required": True})
50
+
51
+ # EU AI Act (RFC-028)
52
+ client.declare_regulatory_context(RegulatoryContext(
53
+ active=True, framework="EU_AI_ACT", risk_classification="HIGH_RISK",
54
+ deployer_jurisdiction="DE",
55
+ ))
56
+
57
+ # Japan AI Promotion Act (RFC-030)
58
+ client.declare_japanese_context(JapaneseContext(
59
+ active=True, governance_framework="JAPAN_AI_PROMOTION_ACT",
60
+ sector="HEALTHCARE", innovation_priority=True,
61
+ ))
62
+
63
+ # AU Continental AI Strategy (RFC-029)
64
+ client.declare_african_context(AfricanContext(
65
+ active=True, governance_framework="AU_CONTINENTAL_AI_STRATEGY",
66
+ member_state="KE", ubuntu_principle=True,
67
+ ))
68
+
69
+ # IHL — conflict zones (RFC-027)
70
+ client.declare_humanitarian_context(HumanitarianContext(
71
+ active=True, legal_regime="IHL_GCIV", conflict_classification="NIAC",
72
+ operating_authority="ICRC",
73
+ ))
74
+ ```
75
+
76
+ All subsequent CHECK requests are evaluated against declared contexts. Priority: IHL → EU → AU → Japan → Universal.
77
+
78
+ ## User Feedback (RFC-026)
79
+
80
+ ```python
81
+ await client.feedback(request_id="req-123", session_id="ses-abc",
82
+ rating="thumbs_up", pseudonym_id="user-hash")
83
+ ```
84
+
85
+ ## Multi-Agent Delegation
86
+
87
+ ```python
88
+ from aigp_client import SessionContext, DelegationToken
89
+
90
+ session = SessionContext(user_id="coordinator@company.com")
91
+ child = session.child(scope=["search_only"])
92
+
93
+ token = DelegationToken(issuer="coordinator", delegate="worker",
94
+ allowed_models=["claude-4"], max_tokens=10000)
95
+ encoded = token.encode("shared-secret")
96
+ ```
97
+
98
+ ## Token Tracking
99
+
100
+ ```python
101
+ from aigp_client import TokenAccumulator
102
+
103
+ tokens = TokenAccumulator()
104
+ tokens.add_from_usage({"prompt_tokens": 500, "completion_tokens": 200}) # OpenAI
105
+ tokens.add_from_usage({"inputTokens": 300, "outputTokens": 150}) # Bedrock
106
+ print(f"Total: {tokens.total_input} in, {tokens.total_output} out")
107
+ ```
108
+
109
+ ## Full Lifecycle Trace
110
+
111
+ ```python
112
+ from aigp_client import TraceBuilder
113
+
114
+ tb = TraceBuilder(request_id="req-1")
115
+ tb.start(1); tb.end(1, attributes={"user_id": "user@co.com"}) # Identity
116
+ tb.start(3); tb.end(3, attributes={"decision": "ALLOW"}) # CHECK
117
+ tb.start(9); tb.end(9, attributes={"model": "claude-4"}) # Invoke
118
+ tb.add_tool_span("search_docs", duration_ms=45) # Tool
119
+ tb.start(14); tb.end(14) # RECORD
120
+ spans, summary = tb.build()
121
+ await client.trace(trace_id=tb.trace_id, spans=spans, summary=summary)
122
+ ```
123
+
124
+ ## Tool Governance
125
+
126
+ ```python
127
+ from aigp_client import ToolGovernance
128
+
129
+ tg = ToolGovernance(client=client)
130
+ decision = await tg.check_tool("execute_sql", params={"query": "SELECT *"})
131
+ ```
132
+
133
+ ## Modes
134
+
135
+ | Mode | Behavior | When server unreachable |
136
+ |------|----------|:-:|
137
+ | `REPORT` | Log all, allow all | Allow (fail-open) |
138
+ | `REPORT-TRACE` | Log + stage-level spans | Allow (fail-open) |
139
+ | `ENFORCE` | Check policies, block violations | Deny (fail-closed) |
140
+
141
+ ## Supported Languages
142
+
143
+ | Python | TypeScript | Go | Java | C# | Rust | Ruby | Swift |
144
+ |:------:|:----------:|:--:|:----:|:--:|:----:|:----:|:-----:|
145
+ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
146
+
147
+ All 8 SDKs have: `declareHumanitarianContext`, `declareRegulatoryContext`, `declareAfricanContext`, `declareJapaneseContext`, `declareUniversalContext`.
148
+
149
+ ## License
150
+
151
+ Proprietary — © 2025-2026 Evan Erwee. All rights reserved.
@@ -0,0 +1,64 @@
1
+ """AIGP Client v4.0 — Universal AI Governance Protocol client.
2
+
3
+ Provides:
4
+ - Protocol transport (CHECK/RECORD/TRACE)
5
+ - Session context for multi-agent linking
6
+ - Token accumulation for streaming/multi-cycle
7
+ - Tool governance (per-tool CHECK)
8
+ - Delegation tokens (agent-to-agent scoping)
9
+ - Stream accumulator (periodic RECORD during streaming)
10
+ - Rate-limit backoff
11
+ """
12
+
13
+ __version__ = "4.0.0"
14
+
15
+ # Core protocol
16
+ from .client import AigpClient, GovernanceDecision
17
+ from .trace_builder import TraceBuilder, STAGE_NAMES
18
+ from .context import HumanitarianContext
19
+ from .regulatory import RegulatoryContext
20
+ from .african_context import AfricanContext
21
+ from .japanese_context import JapaneseContext
22
+
23
+ # v3.0+ — Agent support
24
+ from .session import SessionContext
25
+ from .tokens import TokenAccumulator
26
+ from .tools import ToolGovernance
27
+ from .delegation import DelegationToken
28
+ from .streaming import StreamAccumulator
29
+ from .backoff import retry_on_rate_limit
30
+
31
+ # One-line governance wrapper
32
+ from .govern import govern
33
+
34
+ # RFC-032: Anticipation
35
+ from .anticipation import AnticipationBuilder, Criterion
36
+
37
+ # Provider-based config
38
+ from .config import ClientConfig, ClientComponents
39
+ from .providers.model import ModelProviderBase
40
+ from .providers.transport import TransportProviderBase
41
+ from .providers.consent import ConsentHelperBase, TokenMap
42
+ from .schemas import GeoContext
43
+
44
+ __all__ = [
45
+ # Core
46
+ "AigpClient", "GovernanceDecision",
47
+ "TraceBuilder", "STAGE_NAMES",
48
+ "HumanitarianContext",
49
+ "RegulatoryContext",
50
+ "AfricanContext",
51
+ "JapaneseContext",
52
+ # v3.0+ Agent support
53
+ "SessionContext", "TokenAccumulator", "ToolGovernance",
54
+ "DelegationToken", "StreamAccumulator", "retry_on_rate_limit",
55
+ # Config/Providers
56
+ "ClientConfig", "ClientComponents",
57
+ "ModelProviderBase", "TransportProviderBase",
58
+ "ConsentHelperBase", "TokenMap",
59
+ "GeoContext",
60
+ # Governance wrapper
61
+ "govern",
62
+ # RFC-032: Anticipation
63
+ "AnticipationBuilder", "Criterion",
64
+ ]
@@ -0,0 +1,102 @@
1
+ """RFC-029: African Context — typed context for AU Continental AI Strategy alignment.
2
+
3
+ Usage:
4
+ from aigp_client.african_context import AfricanContext
5
+
6
+ ctx = AfricanContext(
7
+ active=True,
8
+ governance_framework="AU_CONTINENTAL_AI_STRATEGY",
9
+ member_state="KE",
10
+ rec="EAC",
11
+ )
12
+ client.declare_african_context(ctx)
13
+ """
14
+
15
+ from dataclasses import dataclass, field
16
+
17
+ VALID_FRAMEWORKS = {"AU_CONTINENTAL_AI_STRATEGY"}
18
+ VALID_RECS = {"EAC", "ECOWAS", "SADC", "COMESA", "AMU", "IGAD", "ECCAS", "CEN-SAD"}
19
+ VALID_SECTORS = {"HEALTH", "AGRICULTURE", "EDUCATION", "PUBLIC_SERVICES", "FINANCE", "ENVIRONMENT"}
20
+ VALID_BENEFICIARIES = {"YOUTH", "WOMEN", "RURAL_COMMUNITIES", "DISABLED", "ELDERLY"}
21
+ VALID_CROSS_BORDER = {"MALABO_CONVENTION", "AU_DATA_POLICY_FRAMEWORK"}
22
+
23
+
24
+ @dataclass
25
+ class AfricanContext:
26
+ """AU Continental AI Strategy context declaration per RFC-029."""
27
+
28
+ active: bool
29
+ governance_framework: str
30
+ member_state: str
31
+ rec: str = ""
32
+ local_first: bool = False
33
+ ubuntu_principle: bool = False
34
+ priority_sector: str = ""
35
+ beneficiary_groups: list[str] = field(default_factory=list)
36
+ language_context: list[str] = field(default_factory=list)
37
+ data_sovereignty: dict = field(default_factory=dict)
38
+ cultural_sensitivity: dict = field(default_factory=dict)
39
+
40
+ def __post_init__(self):
41
+ self.validate()
42
+
43
+ def validate(self) -> None:
44
+ """Raise ValueError if context is invalid."""
45
+ if self.governance_framework not in VALID_FRAMEWORKS:
46
+ raise ValueError(f"Invalid governance_framework: {self.governance_framework}. Must be one of {VALID_FRAMEWORKS}")
47
+ if not self.member_state or len(self.member_state) != 2:
48
+ raise ValueError("member_state must be a 2-letter ISO 3166-1 alpha-2 code")
49
+ if self.rec and self.rec not in VALID_RECS:
50
+ raise ValueError(f"Invalid rec: {self.rec}. Must be one of {VALID_RECS}")
51
+ if self.priority_sector and self.priority_sector not in VALID_SECTORS:
52
+ raise ValueError(f"Invalid priority_sector: {self.priority_sector}. Must be one of {VALID_SECTORS}")
53
+ for bg in self.beneficiary_groups:
54
+ if bg not in VALID_BENEFICIARIES:
55
+ raise ValueError(f"Invalid beneficiary_group: {bg}. Must be one of {VALID_BENEFICIARIES}")
56
+ if self.data_sovereignty:
57
+ cbf = self.data_sovereignty.get("cross_border_framework")
58
+ if cbf and cbf not in VALID_CROSS_BORDER:
59
+ raise ValueError(f"Invalid cross_border_framework: {cbf}. Must be one of {VALID_CROSS_BORDER}")
60
+
61
+ def to_dict(self) -> dict:
62
+ """Serialize for protocol transmission."""
63
+ d = {
64
+ "active": self.active,
65
+ "governance_framework": self.governance_framework,
66
+ "member_state": self.member_state,
67
+ "local_first": self.local_first,
68
+ "ubuntu_principle": self.ubuntu_principle,
69
+ }
70
+ if self.rec:
71
+ d["rec"] = self.rec
72
+ if self.priority_sector:
73
+ d["priority_sector"] = self.priority_sector
74
+ if self.beneficiary_groups:
75
+ d["beneficiary_groups"] = self.beneficiary_groups
76
+ if self.language_context:
77
+ d["language_context"] = self.language_context
78
+ if self.data_sovereignty:
79
+ d["data_sovereignty"] = self.data_sovereignty
80
+ if self.cultural_sensitivity:
81
+ d["cultural_sensitivity"] = self.cultural_sensitivity
82
+ return d
83
+
84
+ def merge(self, request_level: dict) -> "AfricanContext":
85
+ """Merge request-level context. Escalation only — cannot downgrade."""
86
+ if not request_level:
87
+ return self
88
+ if request_level.get("active") is False:
89
+ request_level["active"] = True
90
+ return AfricanContext(
91
+ active=True,
92
+ governance_framework=request_level.get("governance_framework", self.governance_framework),
93
+ member_state=request_level.get("member_state", self.member_state),
94
+ rec=request_level.get("rec", self.rec),
95
+ local_first=request_level.get("local_first", self.local_first),
96
+ ubuntu_principle=request_level.get("ubuntu_principle", self.ubuntu_principle),
97
+ priority_sector=request_level.get("priority_sector", self.priority_sector),
98
+ beneficiary_groups=list(set(self.beneficiary_groups + request_level.get("beneficiary_groups", []))),
99
+ language_context=list(set(self.language_context + request_level.get("language_context", []))),
100
+ data_sovereignty=request_level.get("data_sovereignty", self.data_sovereignty),
101
+ cultural_sensitivity=request_level.get("cultural_sensitivity", self.cultural_sensitivity),
102
+ )