aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a3__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.
Files changed (267) hide show
  1. aethergraph/__init__.py +4 -10
  2. aethergraph/__main__.py +296 -0
  3. aethergraph/api/v1/__init__.py +0 -0
  4. aethergraph/api/v1/agents.py +46 -0
  5. aethergraph/api/v1/apps.py +70 -0
  6. aethergraph/api/v1/artifacts.py +415 -0
  7. aethergraph/api/v1/channels.py +89 -0
  8. aethergraph/api/v1/deps.py +168 -0
  9. aethergraph/api/v1/graphs.py +259 -0
  10. aethergraph/api/v1/identity.py +25 -0
  11. aethergraph/api/v1/memory.py +353 -0
  12. aethergraph/api/v1/misc.py +47 -0
  13. aethergraph/api/v1/pagination.py +29 -0
  14. aethergraph/api/v1/runs.py +568 -0
  15. aethergraph/api/v1/schemas.py +535 -0
  16. aethergraph/api/v1/session.py +323 -0
  17. aethergraph/api/v1/stats.py +201 -0
  18. aethergraph/api/v1/viz.py +152 -0
  19. aethergraph/config/config.py +22 -0
  20. aethergraph/config/loader.py +3 -2
  21. aethergraph/config/storage.py +209 -0
  22. aethergraph/contracts/__init__.py +0 -0
  23. aethergraph/contracts/services/__init__.py +0 -0
  24. aethergraph/contracts/services/artifacts.py +27 -14
  25. aethergraph/contracts/services/memory.py +45 -17
  26. aethergraph/contracts/services/metering.py +129 -0
  27. aethergraph/contracts/services/runs.py +50 -0
  28. aethergraph/contracts/services/sessions.py +87 -0
  29. aethergraph/contracts/services/state_stores.py +3 -0
  30. aethergraph/contracts/services/viz.py +44 -0
  31. aethergraph/contracts/storage/artifact_index.py +88 -0
  32. aethergraph/contracts/storage/artifact_store.py +99 -0
  33. aethergraph/contracts/storage/async_kv.py +34 -0
  34. aethergraph/contracts/storage/blob_store.py +50 -0
  35. aethergraph/contracts/storage/doc_store.py +35 -0
  36. aethergraph/contracts/storage/event_log.py +31 -0
  37. aethergraph/contracts/storage/vector_index.py +48 -0
  38. aethergraph/core/__init__.py +0 -0
  39. aethergraph/core/execution/forward_scheduler.py +13 -2
  40. aethergraph/core/execution/global_scheduler.py +21 -15
  41. aethergraph/core/execution/step_forward.py +10 -1
  42. aethergraph/core/graph/__init__.py +0 -0
  43. aethergraph/core/graph/graph_builder.py +8 -4
  44. aethergraph/core/graph/graph_fn.py +156 -15
  45. aethergraph/core/graph/graph_spec.py +8 -0
  46. aethergraph/core/graph/graphify.py +146 -27
  47. aethergraph/core/graph/node_spec.py +0 -2
  48. aethergraph/core/graph/node_state.py +3 -0
  49. aethergraph/core/graph/task_graph.py +39 -1
  50. aethergraph/core/runtime/__init__.py +0 -0
  51. aethergraph/core/runtime/ad_hoc_context.py +64 -4
  52. aethergraph/core/runtime/base_service.py +28 -4
  53. aethergraph/core/runtime/execution_context.py +13 -15
  54. aethergraph/core/runtime/graph_runner.py +222 -37
  55. aethergraph/core/runtime/node_context.py +510 -6
  56. aethergraph/core/runtime/node_services.py +12 -5
  57. aethergraph/core/runtime/recovery.py +15 -1
  58. aethergraph/core/runtime/run_manager.py +783 -0
  59. aethergraph/core/runtime/run_manager_local.py +204 -0
  60. aethergraph/core/runtime/run_registration.py +2 -2
  61. aethergraph/core/runtime/run_types.py +89 -0
  62. aethergraph/core/runtime/runtime_env.py +136 -7
  63. aethergraph/core/runtime/runtime_metering.py +71 -0
  64. aethergraph/core/runtime/runtime_registry.py +36 -13
  65. aethergraph/core/runtime/runtime_services.py +194 -6
  66. aethergraph/core/tools/builtins/toolset.py +1 -1
  67. aethergraph/core/tools/toolkit.py +5 -0
  68. aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
  69. aethergraph/plugins/agents/default_chat_agent.py +171 -0
  70. aethergraph/plugins/agents/shared.py +81 -0
  71. aethergraph/plugins/channel/adapters/webui.py +112 -112
  72. aethergraph/plugins/channel/routes/webui_routes.py +367 -102
  73. aethergraph/plugins/channel/utils/slack_utils.py +115 -59
  74. aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
  75. aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
  76. aethergraph/runtime/__init__.py +15 -0
  77. aethergraph/server/app_factory.py +196 -34
  78. aethergraph/server/clients/channel_client.py +202 -0
  79. aethergraph/server/http/channel_http_routes.py +116 -0
  80. aethergraph/server/http/channel_ws_routers.py +45 -0
  81. aethergraph/server/loading.py +117 -0
  82. aethergraph/server/server.py +131 -0
  83. aethergraph/server/server_state.py +240 -0
  84. aethergraph/server/start.py +227 -66
  85. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  86. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  87. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  88. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  89. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  90. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  91. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  92. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  93. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  94. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  95. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  96. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  97. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  98. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  99. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  100. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  101. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  102. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  103. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  104. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  105. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  106. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  107. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  108. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  109. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  110. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  111. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  112. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  113. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  114. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  115. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  116. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  117. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  118. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  119. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  120. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  121. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  122. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  123. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  124. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  125. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  126. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  127. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  128. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  129. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  130. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  131. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  132. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  133. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  134. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  135. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  136. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  137. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  138. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  139. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  140. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  141. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  142. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  143. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  144. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
  145. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
  146. aethergraph/server/ui_static/index.html +15 -0
  147. aethergraph/server/ui_static/logo.png +0 -0
  148. aethergraph/services/artifacts/__init__.py +0 -0
  149. aethergraph/services/artifacts/facade.py +1239 -132
  150. aethergraph/services/auth/{dev.py → authn.py} +0 -8
  151. aethergraph/services/auth/authz.py +100 -0
  152. aethergraph/services/channel/__init__.py +0 -0
  153. aethergraph/services/channel/channel_bus.py +19 -1
  154. aethergraph/services/channel/factory.py +13 -1
  155. aethergraph/services/channel/ingress.py +311 -0
  156. aethergraph/services/channel/queue_adapter.py +75 -0
  157. aethergraph/services/channel/session.py +502 -19
  158. aethergraph/services/container/default_container.py +122 -43
  159. aethergraph/services/continuations/continuation.py +6 -0
  160. aethergraph/services/continuations/stores/fs_store.py +19 -0
  161. aethergraph/services/eventhub/event_hub.py +76 -0
  162. aethergraph/services/kv/__init__.py +0 -0
  163. aethergraph/services/kv/ephemeral.py +244 -0
  164. aethergraph/services/llm/__init__.py +0 -0
  165. aethergraph/services/llm/generic_client copy.py +691 -0
  166. aethergraph/services/llm/generic_client.py +1288 -187
  167. aethergraph/services/llm/providers.py +3 -1
  168. aethergraph/services/llm/types.py +47 -0
  169. aethergraph/services/llm/utils.py +284 -0
  170. aethergraph/services/logger/std.py +3 -0
  171. aethergraph/services/mcp/__init__.py +9 -0
  172. aethergraph/services/mcp/http_client.py +38 -0
  173. aethergraph/services/mcp/service.py +225 -1
  174. aethergraph/services/mcp/stdio_client.py +41 -6
  175. aethergraph/services/mcp/ws_client.py +44 -2
  176. aethergraph/services/memory/__init__.py +0 -0
  177. aethergraph/services/memory/distillers/llm_long_term.py +234 -0
  178. aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
  179. aethergraph/services/memory/distillers/long_term.py +225 -0
  180. aethergraph/services/memory/facade/__init__.py +3 -0
  181. aethergraph/services/memory/facade/chat.py +440 -0
  182. aethergraph/services/memory/facade/core.py +447 -0
  183. aethergraph/services/memory/facade/distillation.py +424 -0
  184. aethergraph/services/memory/facade/rag.py +410 -0
  185. aethergraph/services/memory/facade/results.py +315 -0
  186. aethergraph/services/memory/facade/retrieval.py +139 -0
  187. aethergraph/services/memory/facade/types.py +77 -0
  188. aethergraph/services/memory/facade/utils.py +43 -0
  189. aethergraph/services/memory/facade_dep.py +1539 -0
  190. aethergraph/services/memory/factory.py +9 -3
  191. aethergraph/services/memory/utils.py +10 -0
  192. aethergraph/services/metering/eventlog_metering.py +470 -0
  193. aethergraph/services/metering/noop.py +25 -4
  194. aethergraph/services/rag/__init__.py +0 -0
  195. aethergraph/services/rag/facade.py +279 -23
  196. aethergraph/services/rag/index_factory.py +2 -2
  197. aethergraph/services/rag/node_rag.py +317 -0
  198. aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
  199. aethergraph/services/registry/__init__.py +0 -0
  200. aethergraph/services/registry/agent_app_meta.py +419 -0
  201. aethergraph/services/registry/registry_key.py +1 -1
  202. aethergraph/services/registry/unified_registry.py +74 -6
  203. aethergraph/services/scope/scope.py +159 -0
  204. aethergraph/services/scope/scope_factory.py +164 -0
  205. aethergraph/services/state_stores/serialize.py +5 -0
  206. aethergraph/services/state_stores/utils.py +2 -1
  207. aethergraph/services/viz/__init__.py +0 -0
  208. aethergraph/services/viz/facade.py +413 -0
  209. aethergraph/services/viz/viz_service.py +69 -0
  210. aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
  211. aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
  212. aethergraph/storage/artifacts/cas_store.py +422 -0
  213. aethergraph/storage/artifacts/fs_cas.py +18 -0
  214. aethergraph/storage/artifacts/s3_cas.py +14 -0
  215. aethergraph/storage/artifacts/utils.py +124 -0
  216. aethergraph/storage/blob/fs_blob.py +86 -0
  217. aethergraph/storage/blob/s3_blob.py +115 -0
  218. aethergraph/storage/continuation_store/fs_cont.py +283 -0
  219. aethergraph/storage/continuation_store/inmem_cont.py +146 -0
  220. aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
  221. aethergraph/storage/docstore/fs_doc.py +63 -0
  222. aethergraph/storage/docstore/sqlite_doc.py +31 -0
  223. aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
  224. aethergraph/storage/eventlog/fs_event.py +136 -0
  225. aethergraph/storage/eventlog/sqlite_event.py +47 -0
  226. aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
  227. aethergraph/storage/factory.py +432 -0
  228. aethergraph/storage/fs_utils.py +28 -0
  229. aethergraph/storage/graph_state_store/state_store.py +64 -0
  230. aethergraph/storage/kv/inmem_kv.py +103 -0
  231. aethergraph/storage/kv/layered_kv.py +52 -0
  232. aethergraph/storage/kv/sqlite_kv.py +39 -0
  233. aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
  234. aethergraph/storage/memory/event_persist.py +68 -0
  235. aethergraph/storage/memory/fs_persist.py +118 -0
  236. aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
  237. aethergraph/{services → storage}/memory/indices.py +31 -7
  238. aethergraph/storage/metering/meter_event.py +55 -0
  239. aethergraph/storage/runs/doc_store.py +280 -0
  240. aethergraph/storage/runs/inmen_store.py +82 -0
  241. aethergraph/storage/runs/sqlite_run_store.py +403 -0
  242. aethergraph/storage/sessions/doc_store.py +183 -0
  243. aethergraph/storage/sessions/inmem_store.py +110 -0
  244. aethergraph/storage/sessions/sqlite_session_store.py +399 -0
  245. aethergraph/storage/vector_index/chroma_index.py +138 -0
  246. aethergraph/storage/vector_index/faiss_index.py +179 -0
  247. aethergraph/storage/vector_index/sqlite_index.py +187 -0
  248. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a3.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a3.dist-info/entry_points.txt +3 -0
  251. aethergraph/services/artifacts/factory.py +0 -35
  252. aethergraph/services/artifacts/fs_store.py +0 -656
  253. aethergraph/services/artifacts/jsonl_index.py +0 -123
  254. aethergraph/services/artifacts/sqlite_index.py +0 -209
  255. aethergraph/services/memory/distillers/episode.py +0 -116
  256. aethergraph/services/memory/distillers/rolling.py +0 -74
  257. aethergraph/services/memory/facade.py +0 -633
  258. aethergraph/services/memory/persist_fs.py +0 -40
  259. aethergraph/services/rag/index/base.py +0 -27
  260. aethergraph/services/rag/index/faiss_index.py +0 -121
  261. aethergraph/services/rag/index/sqlite_index.py +0 -134
  262. aethergraph-0.1.0a1.dist-info/RECORD +0 -182
  263. aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
  264. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/NOTICE +0 -0
  267. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,3 @@
1
- # services/auth/dev.py
2
1
  class DevTokenAuthn:
3
2
  """Development token authenticator. Accepts any token, returns 'dev' as subject."""
4
3
 
@@ -7,10 +6,3 @@ class DevTokenAuthn:
7
6
 
8
7
  async def whoami(self, token: str | None) -> dict:
9
8
  return {"subject": token or "dev", "roles": ["admin"]}
10
-
11
-
12
- class AllowAllAuthz:
13
- """Development authorizer that allows all actions."""
14
-
15
- async def allow(self, actor: dict, action: str, resource: str) -> bool:
16
- return True
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Literal, Protocol
5
+
6
+ from fastapi import HTTPException, status
7
+
8
+ if TYPE_CHECKING:
9
+ from aethergraph.api.v1.deps import RequestIdentity
10
+
11
+ AuthZServiceScope = Literal["runs", "artifacts", "graphs", "admin", "system"]
12
+ AuthZServiceAction = Literal["read", "write", "execute", "delete", "admin"]
13
+
14
+
15
+ class AuthZService(Protocol):
16
+ async def authorize(
17
+ self,
18
+ *,
19
+ identity: RequestIdentity,
20
+ scope: AuthZServiceScope,
21
+ action: AuthZServiceAction,
22
+ ) -> None:
23
+ """Authorize the given identity to perform the action within the scope.
24
+
25
+ Raises HTTPException with status 403 if not authorized.
26
+ """
27
+ ...
28
+
29
+
30
+ @dataclass
31
+ class AllowAllAuthz(AuthZService):
32
+ """
33
+ Default OSS-safe behavior: everything is allowed.
34
+ Useful for local/demo, and as a safe fallback if authz isn't configured.
35
+ """
36
+
37
+ async def authorize(
38
+ self,
39
+ *,
40
+ identity: RequestIdentity,
41
+ scope: AuthZServiceScope,
42
+ action: AuthZServiceAction,
43
+ ) -> None:
44
+ """Always allow."""
45
+ return
46
+
47
+
48
+ @dataclass
49
+ class BasicAuthz(AuthZService):
50
+ """
51
+ Minimal policy based on mode and roles:
52
+ - local: allow everything
53
+ - demo: allow normal operations, block admin/system stuff
54
+ - cloud: allow everything except admin unless role "admin" is present
55
+ """
56
+
57
+ allow_local_admin: bool = False
58
+
59
+ async def authorize(
60
+ self,
61
+ *,
62
+ identity: RequestIdentity,
63
+ scope: AuthZServiceScope,
64
+ action: AuthZServiceAction,
65
+ ) -> None:
66
+ # Local dev: basically god-mode
67
+ if identity.mode == "local":
68
+ if self.allow_local_admin:
69
+ return
70
+ if scope != "admin":
71
+ return
72
+
73
+ # Demo mode: no admin / system endpoints
74
+ if identity.mode == "demo":
75
+ if scope in ("admin", "system"):
76
+ raise HTTPException(
77
+ status_code=status.HTTP_403_FORBIDDEN,
78
+ detail="Admin/system operations are disabled in demo mode.",
79
+ )
80
+ return
81
+
82
+ # Cloud mode: basic, role-based admin
83
+ if identity.mode == "cloud":
84
+ if scope in ("admin", "system"):
85
+ if "admin" not in identity.roles:
86
+ raise HTTPException(
87
+ status_code=status.HTTP_403_FORBIDDEN,
88
+ detail="Admin privileges required.",
89
+ )
90
+ return
91
+
92
+ # Non-admin scopes: for now, allow everything.
93
+ # Later you can restrict per-org / per-resource here.
94
+ return
95
+
96
+ # Fallback: if someone invents a new mode and forgets to handle it
97
+ raise HTTPException(
98
+ status_code=status.HTTP_403_FORBIDDEN,
99
+ detail=f"Unknown auth mode: {identity.mode}",
100
+ )
File without changes
@@ -212,10 +212,28 @@ class ChannelBus:
212
212
  "resume_key": resume_key,
213
213
  }
214
214
 
215
+ # Enrich continuation meta with the same context fields we attach
216
+ # on normal channel events (if present on the continuation object).
217
+ session_id = getattr(continuation, "session_id", None)
218
+ if session_id is not None:
219
+ meta.setdefault("session_id", session_id)
220
+
221
+ agent_id = getattr(continuation, "agent_id", None)
222
+ if agent_id is not None:
223
+ meta.setdefault("agent_id", agent_id)
224
+
225
+ app_id = getattr(continuation, "app_id", None)
226
+ if app_id is not None:
227
+ meta.setdefault("app_id", app_id)
228
+
229
+ graph_id = getattr(continuation, "graph_id", None)
230
+ if graph_id is not None:
231
+ meta.setdefault("graph_id", graph_id)
232
+
215
233
  # Shape event
216
234
  if kind == "user_input":
217
235
  silent = False
218
- if hasattr(continuation, "payload"):
236
+ if hasattr(continuation, "payload") and isinstance(continuation.payload, dict):
219
237
  silent = continuation.payload.get("_silent", False)
220
238
 
221
239
  txt = prompt if isinstance(prompt, str) else None
@@ -1,8 +1,11 @@
1
1
  # channels/factory.py
2
+ from __future__ import annotations
3
+
2
4
  import os
3
5
  from typing import Any
4
6
 
5
7
  from aethergraph.config.config import AppSettings
8
+ from aethergraph.contracts.storage.event_log import EventLog
6
9
  from aethergraph.plugins.channel.adapters.console import ConsoleChannelAdapter
7
10
  from aethergraph.plugins.channel.adapters.file import FileChannelAdapter
8
11
  from aethergraph.plugins.channel.adapters.slack import SlackChannelAdapter
@@ -11,7 +14,9 @@ from aethergraph.plugins.channel.adapters.webhook import WebhookChannelAdapter
11
14
  from aethergraph.services.channel.channel_bus import ChannelBus
12
15
 
13
16
 
14
- def make_channel_adapters_from_env(cfg: AppSettings) -> dict[str, Any]:
17
+ def make_channel_adapters_from_env(
18
+ cfg: AppSettings, event_log: EventLog | None = None
19
+ ) -> dict[str, Any]:
15
20
  # Always include console adapter
16
21
  adapters = {"console": ConsoleChannelAdapter()}
17
22
 
@@ -29,6 +34,13 @@ def make_channel_adapters_from_env(cfg: AppSettings) -> dict[str, Any]:
29
34
 
30
35
  # include webhook adapter
31
36
  adapters["webhook"] = WebhookChannelAdapter()
37
+
38
+ # Always include webui adapter
39
+ from aethergraph.plugins.channel.adapters.webui import WebUIChannelAdapter
40
+
41
+ if event_log is None:
42
+ raise ValueError("event_log must be provided to create WebUIChannelAdapter")
43
+ adapters["ui"] = WebUIChannelAdapter(event_log=event_log)
32
44
  return adapters
33
45
 
34
46
 
@@ -0,0 +1,311 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import aiohttp
8
+
9
+ from aethergraph.services.continuations.continuation import Continuation, Correlator
10
+
11
+
12
+ @dataclass
13
+ class IncomingFile:
14
+ """
15
+ Generic description of a file coming from an external UI.
16
+
17
+ You can:
18
+ - pre-upload somewhere and pass url/uri, or
19
+ - provide a public url and let AG download + store as artifact.
20
+ """
21
+
22
+ id: str | None = None # Optional identifier for the file
23
+ name: str | None = None # Optional name of the file
24
+ mimetype: str | None = None # Optional MIME type of the file
25
+ size: int | None = None # Optional size of the file in bytes
26
+ url: str | None = None # URL where the file is located
27
+ uri: str | None = None # URI where the file is located
28
+ extra: dict[str, Any] = None # Any extra metadata
29
+
30
+
31
+ @dataclass
32
+ class IncomingMessage:
33
+ """
34
+ Transport-agnostic inbound message shape.
35
+ Used by HTTP/WS handlers and any custom code that wants to resume via channel.
36
+ """
37
+
38
+ scheme: str # e.g. "ext", "mychat", "slack-http", etc.
39
+ channel_id: str # Channel identifier
40
+ thread_id: str | None = None # Optional thread/conversation identifier
41
+
42
+ # For ask_text / ask_file continuations
43
+ text: str | None = None # Text content of the message
44
+ files: Iterable[IncomingFile] | None = None # Attached files
45
+
46
+ # For approval
47
+ choice: str | None = None # User's choice/response
48
+
49
+ # Optional structured metadata
50
+ meta: dict[str, Any] | None = None
51
+
52
+
53
+ class ChannelIngress:
54
+ """
55
+ Canonical entry point for inbound messages from external channels.
56
+
57
+ Typical flow:
58
+ UI -> HTTP/WS -> ChannelIngress.handle(...) -> cont_store + resume_router
59
+ """
60
+
61
+ def __init__(self, *, container, logger=None):
62
+ self.c = container
63
+ # Validate and assign dependencies
64
+
65
+ assert container is not None, "Either provide all dependencies or a container"
66
+ self.artifacts = container.artifacts if hasattr(container, "artifacts") else None
67
+ self.kv_hot = container.kv_hot if hasattr(container, "kv_hot") else None
68
+ self.cont_store = container.cont_store if hasattr(container, "cont_store") else None
69
+ self.resume_router = (
70
+ container.resume_router if hasattr(container, "resume_router") else None
71
+ )
72
+
73
+ if logger is not None:
74
+ self.logger = logger
75
+ else:
76
+ container_logger = getattr(container, "logger", None)
77
+ self.logger = container_logger.for_channel() if container_logger else None
78
+
79
+ def _channel_key(self, scheme: str, channel_id: str) -> str:
80
+ """
81
+ Build a canonical channel key string from scheme + channel_id.
82
+
83
+ - For the generic "ext" channel, we use "ext:chan/<id>".
84
+ - For Slack/Telegram/etc. we can just use "<scheme>:<channel_id>" so we can
85
+ preserve their existing formats.
86
+ """
87
+ if scheme == "ext":
88
+ return f"{scheme}:chan/{channel_id}"
89
+ # Slack: channel_id = "team/T:chan/C" => "slack:team/T:chan/C"
90
+ # Telegram: channel_id = "chat/<id>[:topic/<topic_id>]" => "tg:chat/..."
91
+ return f"{scheme}:{channel_id}"
92
+
93
+ def _log(self, level: str, msg: str, **kwargs):
94
+ if not self.logger:
95
+ print(f"[{level.upper()}] {msg} | {kwargs}")
96
+ return
97
+ log_fn = getattr(self.logger, level.lower(), self.logger.info)
98
+ log_fn(msg, extra=kwargs)
99
+
100
+ async def _download_url(self, url: str) -> bytes:
101
+ """
102
+ Simple downloader for public URLs.
103
+ """
104
+ async with aiohttp.ClientSession() as sess, sess.get(url) as r:
105
+ r.raise_for_status()
106
+ return await r.read()
107
+
108
+ async def _stage_file(
109
+ self,
110
+ *,
111
+ data: bytes,
112
+ file_id: str | None,
113
+ name: str,
114
+ ch_key: str,
115
+ cont: Continuation,
116
+ ) -> str:
117
+ """
118
+ Write bytes to tmp path, then save via ArtifactStore.save_file(...).
119
+ Returns the Artifact.uri (string).
120
+ """
121
+ tmp = await self.artifacts.plan_staging_path(planned_ext=f"_{file_id or name}")
122
+
123
+ with open(tmp, "wb") as f:
124
+ f.write(data)
125
+
126
+ run_id = cont.run_id if cont else "ad-hoc"
127
+ node_id = cont.node_id if cont else "channel-ingress"
128
+
129
+ art = await self.artifacts.save_file(
130
+ path=tmp,
131
+ kind="upload",
132
+ run_id=run_id,
133
+ graph_id="channel",
134
+ node_id=node_id,
135
+ tool_name="channel.upload",
136
+ tool_version="0.0.1",
137
+ suggested_uri=None,
138
+ pin=False,
139
+ labels={
140
+ "source": "channel",
141
+ "channel": ch_key,
142
+ "name": name,
143
+ "inbound_file_id": file_id or "",
144
+ },
145
+ metrics=None,
146
+ preview_uri=None,
147
+ )
148
+
149
+ saved_uri = getattr(art, "uri", None)
150
+ if not saved_uri:
151
+ self._log(
152
+ "error",
153
+ "Failed to save uploaded file as artifact",
154
+ channel=ch_key,
155
+ )
156
+
157
+ return saved_uri
158
+
159
+ async def _handle_files(
160
+ self,
161
+ msg: IncomingMessage,
162
+ *,
163
+ ch_key: str,
164
+ cont: Continuation,
165
+ ) -> list[dict[str, Any]]:
166
+ """
167
+ Normalize and optionally persist incoming files to artifact store.
168
+
169
+ Returns a list of file_refs that mirror the Slack file_refs shape:
170
+ {id, name, mimetype, size, uri, url, platform, channel_key, ...}
171
+ """
172
+ if not msg.files:
173
+ return []
174
+
175
+ file_refs: list[dict[str, Any]] = []
176
+ for f in msg.files:
177
+ name = f.name or f.id or "unnamed"
178
+ file_id = f.id or name
179
+ mimetype = f.mimetype or "application/octet-stream"
180
+ size = f.size or 0
181
+ uri = f.uri
182
+ url = f.url
183
+
184
+ # Optional: auto-download if url is provided and no uri
185
+ # this is not executed when we stage files with channel-specific upload handlers that already provide uri
186
+ if (not uri) and url:
187
+ try:
188
+ data_bytes = await self._download_url(url)
189
+ uri = await self._stage_file(
190
+ data=data_bytes,
191
+ file_id=file_id,
192
+ name=name,
193
+ ch_key=ch_key,
194
+ cont=cont,
195
+ )
196
+ except Exception as e:
197
+ self._log("warning", f"Ingress: file download failed: {e}", channel_key=ch_key)
198
+
199
+ ref = {
200
+ "id": file_id,
201
+ "name": name,
202
+ "mimetype": mimetype,
203
+ "size": size,
204
+ "uri": uri,
205
+ "url": url,
206
+ "platform": msg.scheme,
207
+ "channel_key": ch_key,
208
+ }
209
+ if f.extra:
210
+ ref["extra"] = dict(f.extra)
211
+
212
+ file_refs.append(ref)
213
+
214
+ # Append to per-channel inbox, dedup by id
215
+ inbox_key = f"inbox://{ch_key}"
216
+ await self.kv_hot.list_append_unique(
217
+ inbox_key,
218
+ file_refs,
219
+ id_key="id",
220
+ )
221
+ return file_refs
222
+
223
+ async def _find_continuation(
224
+ self, *, scheme: str, ch_key: str, thread_id: str | None
225
+ ) -> Continuation | None:
226
+ """
227
+ Find pending continuation for this channel/thread.
228
+ """
229
+ cont = None
230
+ if thread_id:
231
+ corr = Correlator(scheme=scheme, channel=ch_key, thread=thread_id, message="")
232
+ cont = await self.cont_store.find_by_correlator(corr=corr)
233
+
234
+ if not cont:
235
+ # Fallback: look for any continuation for this channel
236
+ corr2 = Correlator(scheme=scheme, channel=ch_key, thread="", message="")
237
+ cont = await self.cont_store.find_by_correlator(corr=corr2)
238
+
239
+ return cont
240
+
241
+ # ---- Public method ----
242
+ async def handle(self, msg: IncomingMessage) -> bool:
243
+ """
244
+ Handle an inbound message and resume a waiting continuation if any.
245
+
246
+ Returns:
247
+ True -> a continuation was found and resumed
248
+ False -> nothing was listening on this channel (fire-and-forget)
249
+ """
250
+ scheme = msg.scheme
251
+ ch_key = self._channel_key(scheme, msg.channel_id)
252
+
253
+ cont = await self._find_continuation(
254
+ scheme=scheme,
255
+ ch_key=ch_key,
256
+ thread_id=msg.thread_id,
257
+ )
258
+
259
+ # Normalize and persist any attached files
260
+ file_refs = []
261
+ if msg.files:
262
+ file_refs = await self._handle_files(
263
+ msg,
264
+ ch_key=ch_key,
265
+ cont=cont,
266
+ )
267
+
268
+ if not cont:
269
+ # No continuation found, log and return
270
+ self._log(
271
+ "info",
272
+ "Ingress: no continuation found for inbound message",
273
+ channel_key=ch_key,
274
+ )
275
+ return False
276
+
277
+ # Build payload for resumption
278
+ kind = cont.kind
279
+ meta = msg.meta or {}
280
+
281
+ if kind == "approval":
282
+ choice = (msg.choice or (msg.text or "")).strip() or "reject"
283
+ payload: dict[str, Any] = {
284
+ "choice": choice,
285
+ "channel_key": ch_key,
286
+ "thread_id": msg.thread_id,
287
+ "meta": meta,
288
+ }
289
+ elif kind in ("user_files", "user_input_or_files"):
290
+ payload = {
291
+ "text": msg.text or "",
292
+ "files": file_refs,
293
+ "channel_key": ch_key,
294
+ "thread_id": msg.thread_id,
295
+ "meta": meta,
296
+ }
297
+ else:
298
+ payload = {
299
+ "text": msg.text or "",
300
+ "channel_key": ch_key,
301
+ "thread_id": msg.thread_id,
302
+ "meta": meta,
303
+ }
304
+
305
+ await self.resume_router.resume(
306
+ run_id=cont.run_id,
307
+ node_id=cont.node_id,
308
+ token=cont.token,
309
+ payload=payload,
310
+ )
311
+ return True
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from aethergraph.contracts.services.channel import ChannelAdapter, OutEvent
6
+ from aethergraph.services.continuations.continuation import Correlator
7
+
8
+
9
+ class QueueChannelAdapter(ChannelAdapter):
10
+ """
11
+ Generic adapter that writes OutEvents into a per-channel outbox in kv_hot.
12
+
13
+ This is meant to be paired with:
14
+ - /ws/channel (to stream events to browser/clients)
15
+ - optional /channel/outbox polling endpoint
16
+
17
+ Capabilities: full superset to avoid downgrades in ChannelBus._smart_fallback.
18
+ """
19
+
20
+ # Slack-level capability set; user code can still choose to ignore some fields.
21
+ capabilities: set[str] = {"text", "buttons", "image", "file", "edit", "stream"}
22
+
23
+ def __init__(self, container, *, scheme: str = "ext"):
24
+ self.c = container
25
+ self.scheme = scheme
26
+
27
+ async def send(self, event: OutEvent) -> dict | None:
28
+ """
29
+ Serialize OutEvent to a JSON-friendly dict and append to the channel outbox.
30
+
31
+ Consumers (WS client, HTTP polling, etc.) can render this however they like.
32
+ """
33
+ ch_key = event.channel # expected to already look like "ext:chan/<id>" or similar
34
+ outbox_key = f"outbox://{ch_key}"
35
+
36
+ # Minimal normalization; keep as much info as possible for UI.
37
+ payload: dict[str, Any] = {
38
+ "type": event.type,
39
+ "channel": event.channel,
40
+ "text": event.text,
41
+ "meta": event.meta,
42
+ "rich": event.rich,
43
+ "upsert_key": event.upsert_key,
44
+ "file": event.file,
45
+ "buttons": [],
46
+ }
47
+
48
+ # Buttons: flatten for clients (label/value/style/url)
49
+ if event.buttons:
50
+ btns = []
51
+ for b in event.buttons:
52
+ btns.append(
53
+ {
54
+ "label": getattr(b, "label", None),
55
+ "value": getattr(b, "value", None),
56
+ "style": getattr(b, "style", None),
57
+ "url": getattr(b, "url", None),
58
+ }
59
+ )
60
+ payload["buttons"] = btns
61
+
62
+ # simple timestamp if you have a clock service; otherwise omit
63
+ if hasattr(self.c, "clock"):
64
+ payload["ts"] = self.c.clock.now_ts()
65
+
66
+ await self.c.kv_hot.list_append(outbox_key, [payload])
67
+
68
+ return {
69
+ "correlator": Correlator(
70
+ scheme=self.scheme,
71
+ channel=ch_key,
72
+ thread=(event.meta or {}).get("thread") or "",
73
+ message=None,
74
+ )
75
+ }