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.
- daai_console/__init__.py +44 -0
- daai_console/client.py +237 -0
- daai_console/exceptions.py +36 -0
- daai_console/py.typed +0 -0
- daai_console/runtime/__init__.py +8 -0
- daai_console/runtime/manager.py +53 -0
- daai_console/runtime/runner.py +80 -0
- daai_console/store/__init__.py +8 -0
- daai_console/store/pending.py +138 -0
- daai_console/types.py +79 -0
- daai_console-0.1.0a2.dist-info/METADATA +241 -0
- daai_console-0.1.0a2.dist-info/RECORD +15 -0
- daai_console-0.1.0a2.dist-info/WHEEL +5 -0
- daai_console-0.1.0a2.dist-info/licenses/LICENSE +17 -0
- daai_console-0.1.0a2.dist-info/top_level.txt +1 -0
daai_console/__init__.py
ADDED
|
@@ -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,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,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,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
|