daai-console 0.1.0a2__py3-none-any.whl

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,44 @@
1
+ from daai_console.client import DaaiClient
2
+ from daai_console.exceptions import (
3
+ DaaiApiError,
4
+ DaaiConflictError,
5
+ DaaiError,
6
+ DaaiNotFoundError,
7
+ DaaiUnauthorizedError,
8
+ DaaiValidationError,
9
+ )
10
+ from daai_console.runtime import ActionExecutor, DaaiActionRunner, PendingActionManager
11
+ from daai_console.store import InMemoryPendingStore, PendingAction, PendingStore, SQLitePendingStore
12
+ from daai_console.types import (
13
+ ActionRunStatusResponse,
14
+ ActionTicket,
15
+ ExecutionReportResponse,
16
+ ExecutionStatus,
17
+ GovernanceReceipt,
18
+ GovernanceStatus,
19
+ InterceptResponse,
20
+ )
21
+
22
+ __all__ = [
23
+ "ActionExecutor",
24
+ "ActionRunStatusResponse",
25
+ "ActionTicket",
26
+ "DaaiApiError",
27
+ "DaaiClient",
28
+ "DaaiConflictError",
29
+ "DaaiError",
30
+ "DaaiNotFoundError",
31
+ "DaaiUnauthorizedError",
32
+ "DaaiValidationError",
33
+ "DaaiActionRunner",
34
+ "ExecutionReportResponse",
35
+ "ExecutionStatus",
36
+ "GovernanceReceipt",
37
+ "GovernanceStatus",
38
+ "InMemoryPendingStore",
39
+ "InterceptResponse",
40
+ "PendingAction",
41
+ "PendingActionManager",
42
+ "PendingStore",
43
+ "SQLitePendingStore",
44
+ ]
daai_console/client.py ADDED
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ import httpx
8
+
9
+ from daai_console.exceptions import (
10
+ DaaiApiError,
11
+ DaaiConflictError,
12
+ DaaiError,
13
+ DaaiNotFoundError,
14
+ DaaiUnauthorizedError,
15
+ DaaiValidationError,
16
+ )
17
+ from daai_console.types import (
18
+ ActionRunStatusResponse,
19
+ ExecutionReportResponse,
20
+ ExecutionStatus,
21
+ GovernanceReceipt,
22
+ GovernanceStatus,
23
+ InterceptResponse,
24
+ )
25
+
26
+
27
+ class DaaiClient:
28
+ """Low-level DAAI Console API client for cooperative action governance."""
29
+
30
+ def __init__(
31
+ self,
32
+ base_url: str,
33
+ api_key: str,
34
+ workspace_key: str,
35
+ timeout_seconds: float = 10.0,
36
+ http_client: httpx.Client | None = None,
37
+ ) -> None:
38
+ self._api_key = api_key
39
+ self._workspace_key = workspace_key
40
+ self._owns_client = http_client is None
41
+
42
+ if http_client is not None:
43
+ self._http = http_client
44
+ return
45
+
46
+ self._http = httpx.Client(
47
+ base_url=base_url.rstrip("/"),
48
+ timeout=timeout_seconds,
49
+ )
50
+
51
+ def close(self) -> None:
52
+ if self._owns_client:
53
+ self._http.close()
54
+
55
+ def __enter__(self) -> "DaaiClient":
56
+ return self
57
+
58
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
59
+ self.close()
60
+
61
+ def intercept(
62
+ self,
63
+ action: str,
64
+ payload: dict[str, Any] | None = None,
65
+ idempotency_key: str | None = None,
66
+ ) -> InterceptResponse:
67
+ """Propose a registered action before the developer app executes it."""
68
+ body: dict[str, Any] = {
69
+ "action": action,
70
+ "payload": payload or {},
71
+ }
72
+ if idempotency_key is not None:
73
+ body["idempotency_key"] = idempotency_key
74
+
75
+ data = self._request("POST", "/v1/sdk/intercept", json=body)
76
+ return InterceptResponse(
77
+ action_run_id=UUID(data["action_run_id"]),
78
+ governance_status=GovernanceStatus(data["governance_status"]),
79
+ execution_status=ExecutionStatus(data["execution_status"]),
80
+ governance_reason=data["governance_reason"],
81
+ executable=bool(data["executable"]),
82
+ idempotent_replay=bool(data["idempotent_replay"]),
83
+ receipt=_parse_receipt(data.get("receipt")),
84
+ )
85
+
86
+ def status(self, action_run_id: UUID | str) -> ActionRunStatusResponse:
87
+ """Fetch current governance and execution status for an action run."""
88
+ run_id = str(action_run_id)
89
+ data = self._request("GET", f"/v1/sdk/action-runs/{run_id}/status")
90
+ governance_status = GovernanceStatus(data["governance_status"])
91
+ executable = data.get("executable")
92
+ if executable is None:
93
+ executable = governance_status in (
94
+ GovernanceStatus.ALLOWED,
95
+ GovernanceStatus.APPROVED,
96
+ )
97
+
98
+ return ActionRunStatusResponse(
99
+ action_run_id=UUID(data["action_run_id"]),
100
+ action=data["action"],
101
+ governance_status=governance_status,
102
+ execution_status=ExecutionStatus(data["execution_status"]),
103
+ governance_reason=data["governance_reason"],
104
+ executable=bool(executable),
105
+ receipt=_parse_receipt(data.get("receipt")),
106
+ created_at=_parse_datetime(data["created_at"]),
107
+ decided_at=_parse_datetime(data["decided_at"])
108
+ if data.get("decided_at") is not None
109
+ else None,
110
+ )
111
+
112
+ def report_executed(
113
+ self,
114
+ action_run_id: UUID | str,
115
+ execution_result: dict[str, Any] | None = None,
116
+ ) -> ExecutionReportResponse:
117
+ """Report that the developer-owned executor completed successfully."""
118
+ run_id = str(action_run_id)
119
+ data = self._request(
120
+ "POST",
121
+ f"/v1/sdk/action-runs/{run_id}/report-executed",
122
+ json={"execution_result": execution_result or {}},
123
+ )
124
+ return _parse_execution_report(data)
125
+
126
+ def report_failed(
127
+ self,
128
+ action_run_id: UUID | str,
129
+ execution_error: str,
130
+ execution_result: dict[str, Any] | None = None,
131
+ ) -> ExecutionReportResponse:
132
+ """Report that the developer-owned executor failed after approval/allowance."""
133
+ run_id = str(action_run_id)
134
+ data = self._request(
135
+ "POST",
136
+ f"/v1/sdk/action-runs/{run_id}/report-failed",
137
+ json={
138
+ "execution_error": execution_error,
139
+ "execution_result": execution_result or {},
140
+ },
141
+ )
142
+ return _parse_execution_report(data)
143
+
144
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
145
+ headers = kwargs.pop("headers", {})
146
+ headers.update(
147
+ {
148
+ "Authorization": f"Bearer {self._api_key}",
149
+ "X-DAAI-Workspace-Key": self._workspace_key,
150
+ }
151
+ )
152
+
153
+ try:
154
+ response = self._http.request(method=method, url=path, headers=headers, **kwargs)
155
+ except httpx.HTTPError as exc:
156
+ raise DaaiError(message=str(exc)) from exc
157
+
158
+ if response.is_success:
159
+ data = response.json()
160
+ if not isinstance(data, dict):
161
+ raise DaaiApiError(
162
+ message="unexpected response payload",
163
+ status_code=response.status_code,
164
+ body=data,
165
+ )
166
+ return data
167
+
168
+ self._raise_api_error(response)
169
+ raise RuntimeError("unreachable")
170
+
171
+ def _raise_api_error(self, response: httpx.Response) -> None:
172
+ body: Any
173
+ detail: str
174
+
175
+ try:
176
+ body = response.json()
177
+ except ValueError:
178
+ body = response.text
179
+
180
+ if isinstance(body, dict):
181
+ detail = str(body.get("detail", f"request failed with {response.status_code}"))
182
+ else:
183
+ detail = str(body) if body else f"request failed with {response.status_code}"
184
+
185
+ exception_class: type[DaaiApiError]
186
+ if response.status_code == 401:
187
+ exception_class = DaaiUnauthorizedError
188
+ elif response.status_code == 404:
189
+ exception_class = DaaiNotFoundError
190
+ elif response.status_code == 409:
191
+ exception_class = DaaiConflictError
192
+ elif response.status_code == 422:
193
+ exception_class = DaaiValidationError
194
+ else:
195
+ exception_class = DaaiApiError
196
+
197
+ raise exception_class(
198
+ message=detail,
199
+ status_code=response.status_code,
200
+ body=body,
201
+ )
202
+
203
+
204
+ def _parse_execution_report(data: dict[str, Any]) -> ExecutionReportResponse:
205
+ reported_at_raw = data.get("execution_reported_at")
206
+ reported_at = (
207
+ _parse_datetime(reported_at_raw) if reported_at_raw is not None else None
208
+ )
209
+ return ExecutionReportResponse(
210
+ action_run_id=UUID(data["action_run_id"]),
211
+ execution_status=ExecutionStatus(data["execution_status"]),
212
+ execution_error=data.get("execution_error"),
213
+ execution_reported_at=reported_at,
214
+ idempotent_replay=bool(data["idempotent_replay"]),
215
+ )
216
+
217
+
218
+ def _parse_receipt(raw: dict[str, Any] | None) -> GovernanceReceipt | None:
219
+ if raw is None:
220
+ return None
221
+
222
+ return GovernanceReceipt(
223
+ id=UUID(raw["id"]),
224
+ outcome=raw["outcome"],
225
+ reason=raw["reason"],
226
+ policy_type=raw["policy_type"],
227
+ policy_snapshot=raw["policy_snapshot"],
228
+ created_at=_parse_datetime(raw["created_at"]),
229
+ )
230
+
231
+
232
+ def _parse_datetime(value: str) -> datetime:
233
+ # FastAPI/Pydantic can emit UTC as a trailing "Z"; Python 3.9
234
+ # fromisoformat expects "+00:00", so normalize first.
235
+ if value.endswith("Z"):
236
+ value = f"{value[:-1]}+00:00"
237
+ return datetime.fromisoformat(value)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class DaaiError(Exception):
9
+ message: str
10
+ status_code: int | None = None
11
+ body: Any | None = None
12
+
13
+ def __str__(self) -> str:
14
+ if self.status_code is None:
15
+ return self.message
16
+ return f"{self.status_code}: {self.message}"
17
+
18
+
19
+ class DaaiUnauthorizedError(DaaiError):
20
+ """Raised for 401 API responses."""
21
+
22
+
23
+ class DaaiNotFoundError(DaaiError):
24
+ """Raised for 404 API responses."""
25
+
26
+
27
+ class DaaiConflictError(DaaiError):
28
+ """Raised for 409 API responses."""
29
+
30
+
31
+ class DaaiValidationError(DaaiError):
32
+ """Raised for 422 API responses."""
33
+
34
+
35
+ class DaaiApiError(DaaiError):
36
+ """Raised for other non-success API responses."""
daai_console/py.typed ADDED
File without changes
@@ -0,0 +1,8 @@
1
+ from daai_console.runtime.manager import PendingActionManager
2
+ from daai_console.runtime.runner import ActionExecutor, DaaiActionRunner
3
+
4
+ __all__ = [
5
+ "ActionExecutor",
6
+ "DaaiActionRunner",
7
+ "PendingActionManager",
8
+ ]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from daai_console.client import DaaiClient
7
+ from daai_console.store import PendingAction, PendingStore
8
+ from daai_console.types import ActionTicket, GovernanceStatus
9
+
10
+
11
+ class PendingActionManager:
12
+ def __init__(self, client: DaaiClient, pending_store: PendingStore) -> None:
13
+ self._client = client
14
+ self._pending_store = pending_store
15
+
16
+ def propose(
17
+ self,
18
+ action: str,
19
+ payload: dict[str, Any] | None = None,
20
+ idempotency_key: str | None = None,
21
+ ) -> ActionTicket:
22
+ """Propose an action and persist it locally only when approval is pending."""
23
+ action_payload = payload or {}
24
+ response = self._client.intercept(
25
+ action=action,
26
+ payload=action_payload,
27
+ idempotency_key=idempotency_key,
28
+ )
29
+
30
+ stored_for_later = response.governance_status == GovernanceStatus.PENDING_APPROVAL
31
+ if stored_for_later:
32
+ self._pending_store.put(
33
+ PendingAction(
34
+ action_run_id=response.action_run_id,
35
+ action=action,
36
+ payload=action_payload,
37
+ idempotency_key=idempotency_key,
38
+ created_at=datetime.now(timezone.utc),
39
+ )
40
+ )
41
+
42
+ return ActionTicket(
43
+ action_run_id=response.action_run_id,
44
+ action=action,
45
+ payload=action_payload,
46
+ idempotency_key=idempotency_key,
47
+ governance_status=response.governance_status,
48
+ execution_status=response.execution_status,
49
+ governance_reason=response.governance_reason,
50
+ executable=response.executable,
51
+ idempotent_replay=response.idempotent_replay,
52
+ stored_for_later=stored_for_later,
53
+ )
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from daai_console.client import DaaiClient
6
+ from daai_console.store import PendingStore
7
+ from daai_console.types import ExecutionStatus, GovernanceStatus
8
+
9
+
10
+ ActionExecutor = Callable[[dict[str, Any]], Any]
11
+
12
+
13
+ class DaaiActionRunner:
14
+ def __init__(self, client: DaaiClient, pending_store: PendingStore) -> None:
15
+ self._client = client
16
+ self._pending_store = pending_store
17
+ self._executors: dict[str, ActionExecutor] = {}
18
+
19
+ def when_executable(
20
+ self,
21
+ action: str,
22
+ run: ActionExecutor,
23
+ ) -> None:
24
+ """Register a developer-owned executor for one action name."""
25
+ self._executors[action] = run
26
+
27
+ def run_pending_once(self) -> int:
28
+ """Poll pending actions once and execute only actions marked executable."""
29
+ executed_count = 0
30
+ pending_actions = self._pending_store.list_pending()
31
+
32
+ for pending in pending_actions:
33
+ status = self._client.status(pending.action_run_id)
34
+
35
+ if status.governance_status in (
36
+ GovernanceStatus.REJECTED,
37
+ GovernanceStatus.BLOCKED,
38
+ ):
39
+ self._pending_store.remove(pending.action_run_id)
40
+ continue
41
+
42
+ if status.execution_status in (
43
+ ExecutionStatus.EXECUTED,
44
+ ExecutionStatus.FAILED,
45
+ ):
46
+ self._pending_store.remove(pending.action_run_id)
47
+ continue
48
+
49
+ if not status.executable:
50
+ continue
51
+
52
+ executor = self._executors.get(pending.action)
53
+ if executor is None:
54
+ continue
55
+
56
+ try:
57
+ result = executor(pending.payload)
58
+ execution_result = _normalize_execution_result(result)
59
+ self._client.report_executed(
60
+ action_run_id=pending.action_run_id,
61
+ execution_result=execution_result,
62
+ )
63
+ executed_count += 1
64
+ except Exception as exc:
65
+ self._client.report_failed(
66
+ action_run_id=pending.action_run_id,
67
+ execution_error=f"{type(exc).__name__}: {exc}",
68
+ )
69
+ finally:
70
+ self._pending_store.remove(pending.action_run_id)
71
+
72
+ return executed_count
73
+
74
+
75
+ def _normalize_execution_result(result: Any) -> dict[str, Any]:
76
+ if result is None:
77
+ return {}
78
+ if isinstance(result, dict):
79
+ return result
80
+ return {"result": result}
@@ -0,0 +1,8 @@
1
+ from daai_console.store.pending import InMemoryPendingStore, PendingAction, PendingStore, SQLitePendingStore
2
+
3
+ __all__ = [
4
+ "InMemoryPendingStore",
5
+ "PendingAction",
6
+ "PendingStore",
7
+ "SQLitePendingStore",
8
+ ]
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any, Protocol
9
+ from uuid import UUID
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class PendingAction:
14
+ action_run_id: UUID
15
+ action: str
16
+ payload: dict[str, Any]
17
+ idempotency_key: str | None
18
+ created_at: datetime
19
+
20
+
21
+ class PendingStore(Protocol):
22
+ def put(self, pending_action: PendingAction) -> None: ...
23
+
24
+ def list_pending(self) -> list[PendingAction]: ...
25
+
26
+ def remove(self, action_run_id: UUID | str) -> None: ...
27
+
28
+
29
+ class InMemoryPendingStore:
30
+ def __init__(self) -> None:
31
+ self._pending: dict[str, PendingAction] = {}
32
+
33
+ def put(self, pending_action: PendingAction) -> None:
34
+ self._pending[str(pending_action.action_run_id)] = pending_action
35
+
36
+ def list_pending(self) -> list[PendingAction]:
37
+ return sorted(
38
+ self._pending.values(),
39
+ key=lambda item: item.created_at,
40
+ )
41
+
42
+ def remove(self, action_run_id: UUID | str) -> None:
43
+ self._pending.pop(str(action_run_id), None)
44
+
45
+
46
+ class SQLitePendingStore:
47
+ def __init__(self, db_path: str | Path = "daai_console_pending_actions.sqlite3") -> None:
48
+ self._db_path = str(db_path)
49
+ self._ensure_schema()
50
+
51
+ def put(self, pending_action: PendingAction) -> None:
52
+ payload_json = json.dumps(pending_action.payload, separators=(",", ":"))
53
+
54
+ with self._connect() as conn:
55
+ conn.execute(
56
+ """
57
+ INSERT INTO pending_actions (
58
+ action_run_id,
59
+ action,
60
+ payload_json,
61
+ idempotency_key,
62
+ created_at
63
+ )
64
+ VALUES (?, ?, ?, ?, ?)
65
+ ON CONFLICT(action_run_id) DO UPDATE SET
66
+ action = excluded.action,
67
+ payload_json = excluded.payload_json,
68
+ idempotency_key = excluded.idempotency_key,
69
+ created_at = excluded.created_at
70
+ """,
71
+ (
72
+ str(pending_action.action_run_id),
73
+ pending_action.action,
74
+ payload_json,
75
+ pending_action.idempotency_key,
76
+ pending_action.created_at.isoformat(),
77
+ ),
78
+ )
79
+ conn.commit()
80
+
81
+ def list_pending(self) -> list[PendingAction]:
82
+ with self._connect() as conn:
83
+ rows = conn.execute(
84
+ """
85
+ SELECT action_run_id, action, payload_json, idempotency_key, created_at
86
+ FROM pending_actions
87
+ ORDER BY created_at ASC
88
+ """
89
+ ).fetchall()
90
+
91
+ pending: list[PendingAction] = []
92
+ for row in rows:
93
+ pending.append(
94
+ PendingAction(
95
+ action_run_id=UUID(row["action_run_id"]),
96
+ action=row["action"],
97
+ payload=_parse_payload_json(row["payload_json"]),
98
+ idempotency_key=row["idempotency_key"],
99
+ created_at=datetime.fromisoformat(row["created_at"]),
100
+ )
101
+ )
102
+
103
+ return pending
104
+
105
+ def remove(self, action_run_id: UUID | str) -> None:
106
+ with self._connect() as conn:
107
+ conn.execute(
108
+ "DELETE FROM pending_actions WHERE action_run_id = ?",
109
+ (str(action_run_id),),
110
+ )
111
+ conn.commit()
112
+
113
+ def _connect(self) -> sqlite3.Connection:
114
+ conn = sqlite3.connect(self._db_path)
115
+ conn.row_factory = sqlite3.Row
116
+ return conn
117
+
118
+ def _ensure_schema(self) -> None:
119
+ with self._connect() as conn:
120
+ conn.execute(
121
+ """
122
+ CREATE TABLE IF NOT EXISTS pending_actions (
123
+ action_run_id TEXT PRIMARY KEY,
124
+ action TEXT NOT NULL,
125
+ payload_json TEXT NOT NULL,
126
+ idempotency_key TEXT,
127
+ created_at TEXT NOT NULL
128
+ )
129
+ """
130
+ )
131
+ conn.commit()
132
+
133
+
134
+ def _parse_payload_json(value: str) -> dict[str, Any]:
135
+ parsed = json.loads(value)
136
+ if isinstance(parsed, dict):
137
+ return parsed
138
+ raise ValueError("pending payload must deserialize to an object")
daai_console/types.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Any
7
+ from uuid import UUID
8
+
9
+
10
+ class GovernanceStatus(str, Enum):
11
+ ALLOWED = "allowed"
12
+ PENDING_APPROVAL = "pending_approval"
13
+ APPROVED = "approved"
14
+ REJECTED = "rejected"
15
+ BLOCKED = "blocked"
16
+
17
+
18
+ class ExecutionStatus(str, Enum):
19
+ NOT_EXECUTED = "not_executed"
20
+ AWAITING_EXECUTION_REPORT = "awaiting_execution_report"
21
+ EXECUTED = "executed"
22
+ FAILED = "failed"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class GovernanceReceipt:
27
+ id: UUID
28
+ outcome: str
29
+ reason: str
30
+ policy_type: str
31
+ policy_snapshot: dict[str, Any]
32
+ created_at: datetime
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class InterceptResponse:
37
+ action_run_id: UUID
38
+ governance_status: GovernanceStatus
39
+ execution_status: ExecutionStatus
40
+ governance_reason: str
41
+ executable: bool
42
+ idempotent_replay: bool
43
+ receipt: GovernanceReceipt | None
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ActionRunStatusResponse:
48
+ action_run_id: UUID
49
+ action: str
50
+ governance_status: GovernanceStatus
51
+ execution_status: ExecutionStatus
52
+ governance_reason: str
53
+ executable: bool
54
+ receipt: GovernanceReceipt | None
55
+ created_at: datetime
56
+ decided_at: datetime | None
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class ExecutionReportResponse:
61
+ action_run_id: UUID
62
+ execution_status: ExecutionStatus
63
+ execution_error: str | None
64
+ execution_reported_at: datetime | None
65
+ idempotent_replay: bool
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ActionTicket:
70
+ action_run_id: UUID
71
+ action: str
72
+ payload: dict[str, Any]
73
+ idempotency_key: str | None
74
+ governance_status: GovernanceStatus
75
+ execution_status: ExecutionStatus
76
+ governance_reason: str
77
+ executable: bool
78
+ idempotent_replay: bool
79
+ stored_for_later: bool
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: daai-console
3
+ Version: 0.1.0a2
4
+ Summary: Python SDK for DAAI Console cooperative action governance.
5
+ Author: DAAI Console
6
+ License-Expression: LicenseRef-DAAI-Alpha
7
+ Project-URL: Homepage, https://github.com/sanyAlam/daai-console-python
8
+ Project-URL: Repository, https://github.com/sanyAlam/daai-console-python
9
+ Project-URL: Issues, https://github.com/sanyAlam/daai-console-python/issues
10
+ Keywords: ai,governance,approval,audit,sdk
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: httpx<1.0.0,>=0.28.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: build<2.0.0,>=1.2.0; extra == "dev"
27
+ Requires-Dist: pytest<9.0.0,>=8.4.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # DAAI Console Python SDK
31
+
32
+ DAAI Console is a governance layer for AI automations. It lets developers intercept risky registered agent actions before execution, route them through policy and approval, and produce audit receipts.
33
+
34
+ This repository contains the public alpha Python SDK for DAAI Console.
35
+
36
+ ## What DAAI Console Is Not
37
+
38
+ DAAI Console is intentionally narrow in alpha. It is not:
39
+
40
+ - Magic interception of arbitrary code
41
+ - A browser automation framework
42
+ - A replacement for MCP
43
+ - An OS-wide agent scanner
44
+ - A full enterprise security platform
45
+
46
+ ## Execution Boundary
47
+
48
+ DAAI Console governs proposal, policy, approval, status, and receipts.
49
+
50
+ The developer application owns real business execution, local business logic, executor functions, external integrations, and worker or cron triggers. Place this SDK before risky functions. The SDK does not execute callbacks inside `intercept()`.
51
+
52
+ ## Install
53
+
54
+ From PyPI:
55
+
56
+ ```bash
57
+ pip install daai-console
58
+ ```
59
+
60
+ Alpha pinning:
61
+
62
+ ```bash
63
+ pip install daai-console==0.1.0a2
64
+ ```
65
+
66
+ ## Environment
67
+
68
+ Set these values in your server-side environment:
69
+
70
+ ```bash
71
+ export DAAI_API_KEY="..."
72
+ export DAAI_WORKSPACE_KEY="..."
73
+ export DAAI_BASE_URL="https://your-daai-api.example.com"
74
+ ```
75
+
76
+ Do not expose these values in browser code or frontend bundles.
77
+
78
+ ## Minimal Client Example
79
+
80
+ ```python
81
+ import os
82
+
83
+ from daai_console import DaaiClient
84
+
85
+ client = DaaiClient(
86
+ base_url=os.environ["DAAI_BASE_URL"],
87
+ api_key=os.environ["DAAI_API_KEY"],
88
+ workspace_key=os.environ["DAAI_WORKSPACE_KEY"],
89
+ )
90
+
91
+ payload = {
92
+ "invoice_id": "INV-1025",
93
+ "customer_name": "Acme Finance",
94
+ "amount": 1250,
95
+ }
96
+
97
+ proposal = client.intercept(
98
+ action="send_invoice_reminder",
99
+ payload=payload,
100
+ idempotency_key="invoice-reminder:INV-1025",
101
+ )
102
+
103
+ print(proposal.governance_status.value)
104
+ print(proposal.executable)
105
+
106
+ status = client.status(proposal.action_run_id)
107
+
108
+ if status.executable:
109
+ try:
110
+ # Your app owns the real business execution.
111
+ provider_result = send_invoice_reminder(payload)
112
+ client.report_executed(
113
+ proposal.action_run_id,
114
+ execution_result={"provider_id": provider_result["id"]},
115
+ )
116
+ except Exception as exc:
117
+ client.report_failed(
118
+ proposal.action_run_id,
119
+ execution_error=f"{type(exc).__name__}: {exc}",
120
+ )
121
+ else:
122
+ print("Not executable yet. Wait for approval or policy decision.")
123
+ ```
124
+
125
+ ## Runtime Helper Example
126
+
127
+ The runtime helpers make the safe path easier: propose once, persist pending approvals locally, and run only after DAAI Console reports `executable=true`.
128
+
129
+ ```python
130
+ import os
131
+
132
+ from daai_console import (
133
+ DaaiActionRunner,
134
+ DaaiClient,
135
+ PendingActionManager,
136
+ SQLitePendingStore,
137
+ )
138
+
139
+ client = DaaiClient(
140
+ base_url=os.environ["DAAI_BASE_URL"],
141
+ api_key=os.environ["DAAI_API_KEY"],
142
+ workspace_key=os.environ["DAAI_WORKSPACE_KEY"],
143
+ )
144
+ store = SQLitePendingStore("daai_pending_actions.sqlite3")
145
+ manager = PendingActionManager(client=client, pending_store=store)
146
+
147
+ manager.propose(
148
+ action="send_invoice_reminder",
149
+ payload={"invoice_id": "INV-1025", "amount": 1250},
150
+ idempotency_key="invoice-reminder:INV-1025",
151
+ )
152
+
153
+
154
+ def send_invoice_reminder_executor(payload: dict) -> dict:
155
+ # Keep your existing execution function here.
156
+ return {"message_id": "example-local-result"}
157
+
158
+
159
+ runner = DaaiActionRunner(client=client, pending_store=store)
160
+ runner.when_executable(
161
+ action="send_invoice_reminder",
162
+ run=send_invoice_reminder_executor,
163
+ )
164
+
165
+ executed_count = runner.run_pending_once()
166
+ print(f"Executed {executed_count} approved action(s).")
167
+ ```
168
+
169
+ ## Live Alpha Smoke Tests
170
+
171
+ The unit tests in `tests/` are mocked SDK-internal tests. They do not call DAAI Console and should remain safe for normal local runs and CI.
172
+
173
+ The scripts in `examples/live_*.py` are opt-in live smoke tests for alpha testers with real DAAI Console credentials. They call the configured API and should be run manually against a staging or alpha workspace.
174
+
175
+ Before running live scripts, make sure the action is already registered in your DAAI Console workspace. The default expected action is `send_invoice_reminder`. You can override it with `DAAI_TEST_ACTION_NAME`.
176
+
177
+ Set required environment variables:
178
+
179
+ ```bash
180
+ export DAAI_API_KEY="..."
181
+ export DAAI_WORKSPACE_KEY="..."
182
+ export DAAI_BASE_URL="https://stage.api.daaihq.com"
183
+ ```
184
+
185
+ `DAAI_BASE_URL` must point to the API, not the dashboard. For staging, use `https://stage.api.daaihq.com`, not `https://stage.daaihq.com`.
186
+
187
+ Optional environment variables:
188
+
189
+ ```bash
190
+ export DAAI_TEST_ACTION_NAME="send_invoice_reminder"
191
+ export DAAI_PENDING_DB_PATH="./daai_alpha_pending.db"
192
+ ```
193
+
194
+ Run the low-level client smoke test:
195
+
196
+ ```bash
197
+ python examples/live_client_smoke.py
198
+ ```
199
+
200
+ Run the runtime smoke flow:
201
+
202
+ ```bash
203
+ python examples/live_runtime_smoke.py propose
204
+ python examples/live_runtime_smoke.py list-pending
205
+ ```
206
+
207
+ If the proposal returns `pending_approval`, approve it from the approval email or dashboard. The SDK does not auto-approve and does not bypass governance. After approval, run:
208
+
209
+ ```bash
210
+ python examples/live_runtime_smoke.py run-pending
211
+ ```
212
+
213
+ `run-pending` uses `DaaiActionRunner` and a fake executor. It only simulates execution, and only runs when DAAI Console reports `executable=true`.
214
+
215
+ If the API returns `blocked` or `unknown_action`, it usually means the action is not registered in the workspace, the API/workspace key belongs to another workspace, or the action name does not match exactly. Register the action in the dashboard first, then rerun the script with the same action name.
216
+
217
+ ## Security Notes
218
+
219
+ - Store `DAAI_API_KEY` and `DAAI_WORKSPACE_KEY` server-side only.
220
+ - Do not expose keys in browser or frontend code.
221
+ - Approval links should not contain sensitive payload data.
222
+ - Local pending stores may contain business payloads; treat them as sensitive.
223
+ - The SDK does not execute callbacks inside `intercept()`.
224
+ - Rejected and blocked actions should not execute.
225
+
226
+ ## Known Limitations
227
+
228
+ - Python SDK first.
229
+ - Cooperative interception only.
230
+ - Developers must register and gate actions explicitly.
231
+ - No automatic arbitrary-code interception.
232
+ - Staging alpha APIs may change.
233
+ - No enterprise RBAC in alpha.
234
+
235
+ ## Alpha Docs
236
+
237
+ - [Alpha test checklist](docs/alpha-test-checklist.md)
238
+ - [Security model](docs/security-model.md)
239
+ - [Agency adoption prompt](docs/agency-adoption-prompt.md)
240
+ - [Known limitations](docs/known-limitations.md)
241
+ - [Staging alpha guide](docs/staging-alpha-guide.md)
@@ -0,0 +1,15 @@
1
+ daai_console/__init__.py,sha256=uDacij5zg7hW3poyh-CJM4KXMF0FQRHfhBmgVH2eO00,1127
2
+ daai_console/client.py,sha256=CyZeGwJQE__rixKUa4x1DcXygQ2aQesfRtbDiI3fBps,7883
3
+ daai_console/exceptions.py,sha256=MJpAJF8FgE8EoyTKP0EJSh6WjK4-x1G_m10p9jeLsQo,772
4
+ daai_console/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ daai_console/types.py,sha256=H6qwxdB53mV4nQhGQrF9KzpiUnGoJhgZkHi4jpOFHe0,1837
6
+ daai_console/runtime/__init__.py,sha256=5AYSBLcPE2GgeQt7ITs5ovP5jOgy6LWY0P8a6EDo6XM,224
7
+ daai_console/runtime/manager.py,sha256=4yTEg737j2iA6BAJ1izJKfBcqc23D8BRc38a914n8TY,1891
8
+ daai_console/runtime/runner.py,sha256=tjgf0F1sBXxNxn3EQJSzgsVWUJTa8pacN5dkklXhKoU,2579
9
+ daai_console/store/__init__.py,sha256=Ls5esVt_MhxuuCuhezbgLUKhxNzy9KDpyQTv4C8hxwk,219
10
+ daai_console/store/pending.py,sha256=VbWvtGU1Yv0G5N81mnDjFptDrZ164pGTnu22h4ImqGQ,4389
11
+ daai_console-0.1.0a2.dist-info/licenses/LICENSE,sha256=fKcW6l5A2Alu1LWFp3Od2ClPszT7eJ3OjUx39X5xD4o,863
12
+ daai_console-0.1.0a2.dist-info/METADATA,sha256=8-McO_HFQxhhlIFLW5gcSSxqQw5vdTP5u6sdKcr1EUs,7666
13
+ daai_console-0.1.0a2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ daai_console-0.1.0a2.dist-info/top_level.txt,sha256=3xwhPyK71ROPJZcfDMA3NLqVOJ4OsyGhjud6c6kUo7U,13
15
+ daai_console-0.1.0a2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,17 @@
1
+ DAAI Console Alpha SDK License
2
+
3
+ Copyright (c) 2026 DAAI Console.
4
+
5
+ This SDK is provided for alpha evaluation of DAAI Console. You may use, copy,
6
+ and modify this SDK for the purpose of evaluating, testing, and integrating
7
+ DAAI Console during the alpha period.
8
+
9
+ Redistribution, resale, or production use outside an active DAAI Console alpha
10
+ or written agreement requires prior written permission from DAAI Console.
11
+
12
+ THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
14
+ PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER
16
+ IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
17
+ CONNECTION WITH THE SDK OR THE USE OR OTHER DEALINGS IN THE SDK.
@@ -0,0 +1 @@
1
+ daai_console