agp-sdk 0.5.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,7 @@
1
+ **/.DS_Store
2
+ .vscode/
3
+ _site/
4
+ .vercel/
5
+ **/__pycache__/
6
+ **/*.pyc
7
+ .vercel
agp_sdk-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: agp-sdk
3
+ Version: 0.5.0
4
+ Summary: Python SDK for the Agent Governance Protocol
5
+ Project-URL: Homepage, https://agp.dev
6
+ Project-URL: Repository, https://github.com/cunardai/agp
7
+ Project-URL: Bug Tracker, https://github.com/cunardai/agp/issues
8
+ License: Apache-2.0
9
+ Keywords: accountability,agents,agp,ai,compliance,governance
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: pydantic>=2.7.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: respx>=0.21; extra == 'dev'
@@ -0,0 +1,79 @@
1
+ """
2
+ AGP Python SDK — Agent Governance Protocol
3
+
4
+ https://agp.dev
5
+ """
6
+ from .client import AGPClient
7
+ from .exceptions import (
8
+ AGPApprovalRequiredError,
9
+ AGPBadRequestError,
10
+ AGPConflictError,
11
+ AGPConnectionError,
12
+ AGPError,
13
+ AGPNotFoundError,
14
+ AGPPolicyDeniedError,
15
+ AGPServerError,
16
+ AGPTokenExpiredError,
17
+ AGPTokenRevokedError,
18
+ AGPUnprocessableError,
19
+ )
20
+ from .models import (
21
+ ActionEnvelope,
22
+ ApprovalArtifact,
23
+ AsyncOperation,
24
+ AttestedContext,
25
+ CapabilityToken,
26
+ DecisionRecord,
27
+ DelegationChain,
28
+ EscalationNotice,
29
+ EvidenceBundle,
30
+ EventLedgerRecord,
31
+ ExecutionReceipt,
32
+ GovernanceAttestation,
33
+ LiabilityBinding,
34
+ PagedResult,
35
+ PolicyDecision,
36
+ PolicySet,
37
+ RevocationNotice,
38
+ SkillManifest,
39
+ Task,
40
+ )
41
+ from .session import TaskSession
42
+
43
+ __version__ = "0.5.0"
44
+ __all__ = [
45
+ "AGPClient",
46
+ "TaskSession",
47
+ # Exceptions
48
+ "AGPError",
49
+ "AGPNotFoundError",
50
+ "AGPBadRequestError",
51
+ "AGPConflictError",
52
+ "AGPUnprocessableError",
53
+ "AGPPolicyDeniedError",
54
+ "AGPTokenRevokedError",
55
+ "AGPTokenExpiredError",
56
+ "AGPApprovalRequiredError",
57
+ "AGPConnectionError",
58
+ "AGPServerError",
59
+ # Models
60
+ "Task",
61
+ "LiabilityBinding",
62
+ "CapabilityToken",
63
+ "DelegationChain",
64
+ "SkillManifest",
65
+ "PolicySet",
66
+ "RevocationNotice",
67
+ "AttestedContext",
68
+ "EvidenceBundle",
69
+ "DecisionRecord",
70
+ "PolicyDecision",
71
+ "ApprovalArtifact",
72
+ "EscalationNotice",
73
+ "ActionEnvelope",
74
+ "ExecutionReceipt",
75
+ "EventLedgerRecord",
76
+ "GovernanceAttestation",
77
+ "AsyncOperation",
78
+ "PagedResult",
79
+ ]
@@ -0,0 +1,124 @@
1
+ """
2
+ AGP SDK — AGPClient
3
+
4
+ Top-level entry point. Composes RegistryClient, DecisionClient,
5
+ ExecutionClient, and TaskSession into a single object.
6
+
7
+ Usage:
8
+ from agp import AGPClient
9
+
10
+ client = AGPClient("http://localhost:8099")
11
+
12
+ # High-level session API
13
+ with client.task_session(
14
+ principal_id="my-agent",
15
+ requested_outcome="analyse sentiment",
16
+ risk_tier="low",
17
+ ) as session:
18
+ session.bind(sponsoring_entity="acme", accountable_owner="cto@acme.com",
19
+ jurisdiction="EU")
20
+ session.decide(agent_id="my-agent", selected_action="analyse",
21
+ rationale="safe", uncertainty_score=0.05)
22
+ session.evaluate(verdict="allow")
23
+ receipt = session.execute(agent_id="my-agent", tool_id="nlp",
24
+ operation={}, capability_token_ref="cap_xxx")
25
+
26
+ # Low-level direct API
27
+ task = client.registry.create_task(...)
28
+ decision = client.decision.create_decision(...)
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from typing import Any
33
+
34
+ from .decision import DecisionClient
35
+ from .execution import ExecutionClient
36
+ from .http import Transport
37
+ from .models import RiskTier, Task
38
+ from .registry import RegistryClient
39
+ from .session import TaskSession
40
+
41
+
42
+ class AGPClient:
43
+ """
44
+ Main AGP client. Thread-safe for concurrent use.
45
+
46
+ Args:
47
+ base_url: URL of the AGP server (e.g. "http://localhost:8099")
48
+ timeout: HTTP request timeout in seconds (default 30)
49
+ headers: Additional headers to send with every request
50
+ (e.g. {"Authorization": "Bearer <token>"})
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ base_url: str,
56
+ *,
57
+ timeout: float = 30.0,
58
+ headers: dict[str, str] | None = None,
59
+ ):
60
+ self._transport = Transport(base_url, timeout=timeout, headers=headers)
61
+ self.registry = RegistryClient(self._transport)
62
+ self.decision = DecisionClient(self._transport)
63
+ self.execution = ExecutionClient(self._transport)
64
+
65
+ def close(self) -> None:
66
+ self._transport.close()
67
+
68
+ def __enter__(self) -> "AGPClient":
69
+ return self
70
+
71
+ def __exit__(self, *_: Any) -> None:
72
+ self.close()
73
+
74
+ # ── TaskSession factory ────────────────────────────────────────────────
75
+
76
+ def task_session(
77
+ self,
78
+ *,
79
+ principal_id: str,
80
+ requested_outcome: str,
81
+ risk_tier: RiskTier,
82
+ expires_at: str | None = None,
83
+ sponsoring_entity: str | None = None,
84
+ governance_requirements: dict[str, Any] | None = None,
85
+ jurisdictions: list[str] | None = None,
86
+ regulatory_frameworks: list[str] | None = None,
87
+ idempotency_key: str | None = None,
88
+ ) -> TaskSession:
89
+ """
90
+ Create a new task and return a TaskSession bound to it.
91
+
92
+ The session can be used as a context manager — on unhandled exception
93
+ it will attempt to mark the task FAILED.
94
+ """
95
+ task = self.registry.create_task(
96
+ principal_id=principal_id,
97
+ requested_outcome=requested_outcome,
98
+ risk_tier=risk_tier,
99
+ expires_at=expires_at,
100
+ sponsoring_entity=sponsoring_entity,
101
+ governance_requirements=governance_requirements,
102
+ jurisdictions=jurisdictions,
103
+ regulatory_frameworks=regulatory_frameworks,
104
+ idempotency_key=idempotency_key,
105
+ )
106
+ return TaskSession(
107
+ registry=self.registry,
108
+ decision=self.decision,
109
+ execution=self.execution,
110
+ task=task,
111
+ )
112
+
113
+ def resume_session(self, task_id: str) -> TaskSession:
114
+ """
115
+ Resume a TaskSession for an existing task (e.g. after a restart
116
+ or when picking up an APPROVAL_PENDING task from a queue).
117
+ """
118
+ task = self.registry.get_task(task_id)
119
+ return TaskSession(
120
+ registry=self.registry,
121
+ decision=self.decision,
122
+ execution=self.execution,
123
+ task=task,
124
+ )
@@ -0,0 +1,181 @@
1
+ """
2
+ AGP SDK — Decision API client
3
+
4
+ Wraps: /agp/decision/contexts, /evidence-bundles, /decisions,
5
+ /policy-evaluations, /approvals, /escalations
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime, timezone
10
+ from typing import Any
11
+
12
+ from .http import Transport, new_idempotency_key
13
+ from .models import (
14
+ ApprovalArtifact, ApprovalDecision, AttestedContext, DecisionRecord,
15
+ EscalationNotice, EvidenceBundle, PolicyDecision, Verdict,
16
+ )
17
+
18
+
19
+ def _now() -> str:
20
+ return datetime.now(timezone.utc).isoformat()
21
+
22
+
23
+ class DecisionClient:
24
+ def __init__(self, transport: Transport):
25
+ self._t = transport
26
+
27
+ # ── Attested Contexts ──────────────────────────────────────────────────
28
+
29
+ def create_context(
30
+ self,
31
+ *,
32
+ task_id: str,
33
+ trust_class: str,
34
+ idempotency_key: str | None = None,
35
+ **extra: Any,
36
+ ) -> AttestedContext:
37
+ body = {"task_id": task_id, "trust_class": trust_class,
38
+ "created_at": _now(), **extra}
39
+ raw = self._t.post("/agp/decision/contexts", body,
40
+ idempotency_key=idempotency_key or new_idempotency_key())
41
+ return AttestedContext.model_validate(raw)
42
+
43
+ def get_context(self, context_id: str) -> AttestedContext:
44
+ return AttestedContext.model_validate(
45
+ self._t.get(f"/agp/decision/contexts/{context_id}")
46
+ )
47
+
48
+ # ── Evidence Bundles ───────────────────────────────────────────────────
49
+
50
+ def create_evidence_bundle(
51
+ self,
52
+ *,
53
+ task_id: str,
54
+ items: list[dict[str, Any]],
55
+ idempotency_key: str | None = None,
56
+ ) -> EvidenceBundle:
57
+ body = {"task_id": task_id, "items": items, "created_at": _now()}
58
+ raw = self._t.post("/agp/decision/evidence-bundles", body,
59
+ idempotency_key=idempotency_key or new_idempotency_key())
60
+ return EvidenceBundle.model_validate(raw)
61
+
62
+ # ── Decisions ──────────────────────────────────────────────────────────
63
+
64
+ def create_decision(
65
+ self,
66
+ *,
67
+ task_id: str,
68
+ agent_id: str,
69
+ selected_action: str,
70
+ rationale: str,
71
+ uncertainty_score: float,
72
+ idempotency_key: str | None = None,
73
+ **extra: Any,
74
+ ) -> DecisionRecord:
75
+ body = {
76
+ "task_id": task_id,
77
+ "agent_id": agent_id,
78
+ "selected_action": selected_action,
79
+ "rationale": rationale,
80
+ "uncertainty_score": uncertainty_score,
81
+ "created_at": _now(),
82
+ **extra,
83
+ }
84
+ raw = self._t.post("/agp/decision/decisions", body,
85
+ idempotency_key=idempotency_key or new_idempotency_key())
86
+ return DecisionRecord.model_validate(raw)
87
+
88
+ def get_decision(self, decision_id: str) -> DecisionRecord:
89
+ return DecisionRecord.model_validate(
90
+ self._t.get(f"/agp/decision/decisions/{decision_id}")
91
+ )
92
+
93
+ # ── Policy Evaluations ─────────────────────────────────────────────────
94
+
95
+ def evaluate_policy(
96
+ self,
97
+ *,
98
+ task_id: str,
99
+ decision_ref: str,
100
+ verdict: Verdict,
101
+ rationale: str | None = None,
102
+ policy_set_ref: str | None = None,
103
+ jurisdiction: str | None = None,
104
+ matched_rules: list[str] | None = None,
105
+ obligations: list[str] | None = None,
106
+ idempotency_key: str | None = None,
107
+ ) -> PolicyDecision:
108
+ body: dict[str, Any] = {
109
+ "task_id": task_id,
110
+ "decision_ref": decision_ref,
111
+ "verdict": verdict,
112
+ "evaluated_at": _now(),
113
+ }
114
+ if rationale:
115
+ body["rationale"] = rationale
116
+ if policy_set_ref:
117
+ body["policy_set_ref"] = policy_set_ref
118
+ if jurisdiction:
119
+ body["jurisdiction"] = jurisdiction
120
+ if matched_rules:
121
+ body["matched_rules"] = matched_rules
122
+ if obligations:
123
+ body["obligations"] = obligations
124
+ raw = self._t.post("/agp/decision/policy-evaluations", body,
125
+ idempotency_key=idempotency_key or new_idempotency_key())
126
+ return PolicyDecision.model_validate(raw)
127
+
128
+ def get_policy_evaluation(self, policy_evaluation_id: str) -> PolicyDecision:
129
+ return PolicyDecision.model_validate(
130
+ self._t.get(f"/agp/decision/policy-evaluations/{policy_evaluation_id}")
131
+ )
132
+
133
+ # ── Approvals ──────────────────────────────────────────────────────────
134
+
135
+ def create_approval(
136
+ self,
137
+ *,
138
+ task_id: str,
139
+ approver_id: str,
140
+ decision: ApprovalDecision,
141
+ notes: str | None = None,
142
+ idempotency_key: str | None = None,
143
+ ) -> ApprovalArtifact:
144
+ body: dict[str, Any] = {
145
+ "task_id": task_id,
146
+ "approver_id": approver_id,
147
+ "decision": decision,
148
+ "approved_at": _now(),
149
+ }
150
+ if notes:
151
+ body["notes"] = notes
152
+ raw = self._t.post("/agp/decision/approvals", body,
153
+ idempotency_key=idempotency_key or new_idempotency_key())
154
+ return ApprovalArtifact.model_validate(raw)
155
+
156
+ def get_approval(self, approval_id: str) -> ApprovalArtifact:
157
+ return ApprovalArtifact.model_validate(
158
+ self._t.get(f"/agp/decision/approvals/{approval_id}")
159
+ )
160
+
161
+ # ── Escalations ────────────────────────────────────────────────────────
162
+
163
+ def escalate(
164
+ self,
165
+ *,
166
+ task_id: str,
167
+ raised_by: str,
168
+ concern_type: str,
169
+ description: str,
170
+ idempotency_key: str | None = None,
171
+ ) -> EscalationNotice:
172
+ body = {
173
+ "task_id": task_id,
174
+ "raised_by": raised_by,
175
+ "concern_type": concern_type,
176
+ "description": description,
177
+ "raised_at": _now(),
178
+ }
179
+ raw = self._t.post("/agp/decision/escalations", body,
180
+ idempotency_key=idempotency_key or new_idempotency_key())
181
+ return EscalationNotice.model_validate(raw)
@@ -0,0 +1,129 @@
1
+ """
2
+ AGP SDK — Exception hierarchy
3
+
4
+ Every AGP error response carries:
5
+ { "error": { "code": "AGP_*", "message": "..." } }
6
+
7
+ This module maps HTTP status codes + AGP error codes to typed exceptions
8
+ so callers can catch specific failure modes without parsing raw JSON.
9
+ """
10
+ from __future__ import annotations
11
+
12
+
13
+ class AGPError(Exception):
14
+ """Base class for all AGP SDK errors."""
15
+
16
+ def __init__(self, message: str, code: str = "AGP_ERROR",
17
+ http_status: int | None = None, retryable: bool = False):
18
+ super().__init__(message)
19
+ self.message = message
20
+ self.code = code
21
+ self.http_status = http_status
22
+ self.retryable = retryable
23
+
24
+ def __repr__(self) -> str:
25
+ return f"{type(self).__name__}(code={self.code!r}, message={self.message!r})"
26
+
27
+
28
+ class AGPNotFoundError(AGPError):
29
+ """404 — The requested resource does not exist."""
30
+ def __init__(self, message: str):
31
+ super().__init__(message, code="AGP_NOT_FOUND", http_status=404)
32
+
33
+
34
+ class AGPBadRequestError(AGPError):
35
+ """400 — Request is malformed or missing required fields."""
36
+ def __init__(self, message: str, code: str = "AGP_INVALID_REQUEST"):
37
+ super().__init__(message, code=code, http_status=400)
38
+
39
+
40
+ class AGPConflictError(AGPError):
41
+ """409 — Request conflicts with current resource state."""
42
+ def __init__(self, message: str, code: str = "AGP_CONFLICT"):
43
+ super().__init__(message, code=code, http_status=409)
44
+
45
+
46
+ class AGPUnprocessableError(AGPError):
47
+ """422 — Semantically invalid (e.g. illegal state transition)."""
48
+ def __init__(self, message: str, code: str = "AGP_INVALID_REQUEST"):
49
+ super().__init__(message, code=code, http_status=422)
50
+
51
+
52
+ class AGPPolicyDeniedError(AGPUnprocessableError):
53
+ """422 AGP_POLICY_DENIED — Execution blocked by policy verdict."""
54
+ def __init__(self, message: str):
55
+ super().__init__(message, code="AGP_POLICY_DENIED")
56
+
57
+
58
+ class AGPTokenRevokedError(AGPUnprocessableError):
59
+ """422 AGP_TOKEN_REVOKED — Capability token has been revoked."""
60
+ def __init__(self, message: str):
61
+ super().__init__(message, code="AGP_TOKEN_REVOKED")
62
+
63
+
64
+ class AGPTokenExpiredError(AGPUnprocessableError):
65
+ """422 AGP_TOKEN_EXPIRED — Capability token has expired."""
66
+ def __init__(self, message: str):
67
+ super().__init__(message, code="AGP_TOKEN_EXPIRED")
68
+
69
+
70
+ class AGPApprovalRequiredError(AGPUnprocessableError):
71
+ """422 AGP_APPROVAL_REQUIRED — Human approval artifact missing or pending."""
72
+ def __init__(self, message: str):
73
+ super().__init__(message, code="AGP_APPROVAL_REQUIRED")
74
+
75
+
76
+ class AGPServerError(AGPError):
77
+ """5xx — Server-side error, may be retryable."""
78
+ def __init__(self, message: str, http_status: int = 500):
79
+ super().__init__(message, code="AGP_SERVER_ERROR",
80
+ http_status=http_status, retryable=True)
81
+
82
+
83
+ class AGPConnectionError(AGPError):
84
+ """Network-level failure reaching the AGP server."""
85
+ def __init__(self, message: str):
86
+ super().__init__(message, code="AGP_CONNECTION_ERROR", retryable=True)
87
+
88
+
89
+ # ── Error code → exception mapping ────────────────────────────────────────────
90
+
91
+ _CODE_MAP: dict[str, type[AGPError]] = {
92
+ "AGP_NOT_FOUND": AGPNotFoundError,
93
+ "AGP_POLICY_DENIED": AGPPolicyDeniedError,
94
+ "AGP_TOKEN_REVOKED": AGPTokenRevokedError,
95
+ "AGP_TOKEN_EXPIRED": AGPTokenExpiredError,
96
+ "AGP_APPROVAL_REQUIRED": AGPApprovalRequiredError,
97
+ "AGP_SCOPE_VIOLATION": AGPConflictError,
98
+ }
99
+
100
+ _STATUS_MAP: dict[int, type[AGPError]] = {
101
+ 400: AGPBadRequestError,
102
+ 404: AGPNotFoundError,
103
+ 409: AGPConflictError,
104
+ 422: AGPUnprocessableError,
105
+ }
106
+
107
+
108
+ def raise_for_response(status: int, body: dict) -> None:
109
+ """Parse an AGP error body and raise the appropriate exception."""
110
+ error = body.get("error", {})
111
+ code = error.get("code", "AGP_ERROR")
112
+ message = error.get("message", f"HTTP {status}")
113
+
114
+ # Specific code takes priority
115
+ if code in _CODE_MAP:
116
+ exc_cls = _CODE_MAP[code]
117
+ if exc_cls is AGPNotFoundError:
118
+ raise exc_cls(message)
119
+ raise exc_cls(message) # type: ignore[call-arg]
120
+
121
+ # Fall back to status code
122
+ exc_cls_by_status = _STATUS_MAP.get(status)
123
+ if exc_cls_by_status:
124
+ raise exc_cls_by_status(message) # type: ignore[call-arg]
125
+
126
+ if status >= 500:
127
+ raise AGPServerError(message, http_status=status)
128
+
129
+ raise AGPError(message, code=code, http_status=status)