create-workframe 0.1.18 → 0.1.21
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.
- package/README.md +1 -1
- package/bin/create-workframe.js +73 -9
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +11 -1
- package/workframe-api/action_proxy.py +8 -0
- package/workframe-api/chat_sessions.py +37 -6
- package/workframe-api/chat_stream.py +16 -0
- package/workframe-api/db_schema.py +29 -0
- package/workframe-api/handler_modules/handler_admin.py +32 -0
- package/workframe-api/handler_modules/handler_auth.py +6 -4
- package/workframe-api/handler_modules/handler_chat.py +17 -26
- package/workframe-api/handler_modules/handler_provider.py +13 -14
- package/workframe-api/handler_modules/handler_workspace.py +60 -2
- package/workframe-api/hermes_profiles.py +31 -9
- package/workframe-api/llm_proxy.py +18 -0
- package/workframe-api/mention_invoke.py +29 -24
- package/workframe-api/message_reactions.py +139 -0
- package/workframe-api/model_surface.py +329 -113
- package/workframe-api/package.json +4 -4
- package/workframe-api/profile_gateway.py +14 -6
- package/workframe-api/provider_bindings.py +118 -17
- package/workframe-api/provider_bootstrap.py +74 -38
- package/workframe-api/provider_model_catalog.py +261 -0
- package/workframe-api/rooms.py +4 -3
- package/workframe-api/route_registry.py +4 -0
- package/workframe-api/runtime_cohort.py +118 -23
- package/workframe-api/scripts/run-python.mjs +28 -0
- package/workframe-api/server.py +44 -12
- package/workframe-api/test_agent_identity_authority.py +96 -0
- package/workframe-api/test_chat_attribution_history.py +59 -0
- package/workframe-api/test_enrich_room_messages.py +43 -2
- package/workframe-api/test_llm_proxy_encoding.py +33 -0
- package/workframe-api/test_message_reactions.py +151 -0
- package/workframe-api/test_model_routing_reconcile.py +122 -0
- package/workframe-api/test_model_surface_consistency.py +45 -5
- package/workframe-api/test_oauth_llm.py +1 -1
- package/workframe-api/test_profile_gateway_health.py +37 -0
- package/workframe-api/test_provider_bindings.py +115 -1
- package/workframe-api/test_provider_model_catalog.py +27 -0
- package/workframe-api/test_provider_model_surface.py +2 -2
- package/workframe-api/test_runtime_lease_lifecycle.py +93 -0
- package/workframe-api/test_server_reexports.py +2 -0
- package/workframe-api/test_user_llm_prefs.py +27 -0
- package/workframe-api/test_user_theme_prefs.py +65 -0
- package/workframe-api/test_workspace_file_actions.py +119 -0
- package/workframe-api/turn_credentials.py +41 -0
- package/workframe-api/turn_overlay.py +95 -34
- package/workframe-api/user_prefs.py +22 -1
- package/workframe-api/workspace_bootstrap.py +1 -0
- package/workframe-api/workspace_files.py +124 -0
- package/workframe-api/zk_auth.py +7 -1
- package/workframe-ui/public/assets/{arc-CSddOtMa.js → arc-D_gXvZHy.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-B8ZI_Iqb.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BSiUei3D.js → architectureDiagram-3BPJPVTR-2F65XJkk.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BhKTULPo.js → blockDiagram-GPEHLZMM-BoURuzr5.js} +1 -1
- package/workframe-ui/public/assets/{brave-B885JYnH.svg → brave-BoYMbZi1.svg} +0 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Bv_xZi-a.js → c4Diagram-AAUBKEIU-BBtveYSm.js} +1 -1
- package/workframe-ui/public/assets/channel-Ca1FvZ7w.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Bh0WiqFE.js → chunk-2J33WTMH-BCuhFg2u.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-qN09pRuK.js → chunk-3OPIFGDE-gM6Q-rjJ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-Dz6FVuMa.js → chunk-4BX2VUAB-D11CJMfF.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-DWtPHVfR.js → chunk-55IACEB6-tOvPBzSf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-CyOUeDAo.js → chunk-5ZQYHXKU-Bb1uF7QP.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BrICvWwx.js → chunk-727SXJPM-BvzzLkGB.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CQu2SmLM.js → chunk-AQP2D5EJ-tr5A2PUD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-uJbYXNNG.js → chunk-BSJP7CBP-BSFEsidk.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CCS_FtVZ.js → chunk-CSCIHK7Q-Bgf-IHHq.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CoLUtPiK.js → chunk-FMBD7UC4-rD27Gf3U.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-DLW4gvEW.js → chunk-KSCS5N6A-C1MobXen.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CMt0LJAd.js → chunk-L5ZTLDWV-CD2wS3bn.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-Bn-maIlt.js → chunk-LZXEDZCA-DQxuSIdE.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CG7yoApR.js → chunk-ND2GUHAM-DHlBi932.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-B4dIvbii.js → chunk-NZK2D7GU-MB8bO5o8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B5spDyXy.js → chunk-O5CBEL6O-BmPwoKAr.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BsuC0swk.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-CyfRhD17.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-D5c0TMX6.js → chunk-XPW4576I-BUCJoags.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DZ_KubnM.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DZ_KubnM.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DFRM3DZv.js → cose-bilkent-S5V4N54A-w9hpFlQU.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-CnUdKeJa.js → dagre-BM42HDAG-DEnBMVrR.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ds3-e-hW.js → diagram-2AECGRRQ-CMoYqkVC.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-B41zx8FF.js → diagram-5GNKFQAL-rS0QQe2R.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D83tI9I8.js → diagram-KO2AKTUF-Do63Vbzi.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-h_EuFyLI.js → diagram-LMA3HP47-Bq3e2FDx.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-CpU-d3RZ.js → diagram-OG6HWLK6-DTESfGaG.js} +1 -1
- package/workframe-ui/public/assets/{dist-_igeSJ9r.js → dist-BiIfSARy.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-jLFoncRU.js → erDiagram-TEJ5UH35-C7RA0P76.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DChy3wGd.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C9IaNgCy.js → flowDiagram-I6XJVG4X-e47AwJAm.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-A9FhfFc5.js → ganttDiagram-6RSMTGT7-BVytj000.js} +1 -1
- package/workframe-ui/public/assets/{gemini-Cq-0-rmr.svg → gemini-DqnZlvMo.svg} +0 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BqK_IjZb.js → gitGraph-WXDBUCRP-QdT2gCGh.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CDVqxhyW.js → gitGraphDiagram-PVQCEYII-COEavQVV.js} +1 -1
- package/workframe-ui/public/assets/index-03WsLxfn.js +133 -0
- package/workframe-ui/public/assets/index-BIKn2D6J.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-C2EKRglL.js → info-J43DQDTF-CEyVZ-9w.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-azQtFzpU.js → infoDiagram-5YYISTIA-CV3ftI3x.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BuDOLv1t.js → ishikawaDiagram-YF4QCWOH-M8CoaInm.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-ypTHFoT0.js → journeyDiagram-JHISSGLW-BtoaC0oj.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Cgr78J_x.js → kanban-definition-UN3LZRKU-Dke-ozKB.js} +1 -1
- package/workframe-ui/public/assets/{line-CcwgaMdv.js → line-CDg_Ms_2.js} +1 -1
- package/workframe-ui/public/assets/{linear-CswIlWaW.js → linear-D3OWfPXS.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CQYhP37X.js → mermaid-parser.core-CGDFs4AZ.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Cz1XNBaK.js → mermaid.core-C9-q_XHF.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BJLu0829.js → mindmap-definition-RKZ34NQL-BvEiBAWz.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-AB1qh96V.js → packet-YPE3B663-FF5nKMbE.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-CokIXLdT.js → pie-LRSECV5Y-CZ9ijSae.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyrwQn5F.js → pieDiagram-4H26LBE5-kD9o509O.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-C_Fdg3F6.js → quadrantDiagram-W4KKPZXB-d_eA7_-T.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C21Xfrrg.js → radar-GUYGQ44K-ViyzC1rg.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ChhaWgaF.js → requirementDiagram-4Y6WPE33-Sq3D_b4Z.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-C5T4sDTt.js → sankeyDiagram-5OEKKPKP-ZZnl3ZwZ.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DRks8i3A.js → sequenceDiagram-3UESZ5HK-Yaz2MgMq.js} +1 -1
- package/workframe-ui/public/assets/{src-BQu5waHd.js → src-DBh8rT2Z.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BI-AKYta.js → stateDiagram-AJRCARHV-DVwi_P3W.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DRSxEx1_.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DnAIWzEc.js → timeline-definition-PNZ67QCA-C88RoMRK.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-BEF_sjx7.js → treeView-BLDUP644-2xabSpow.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BcbL7g5c.js → treemap-LRROVOQU-BA_oRW4e.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DAoIbCkz.js → vennDiagram-CIIHVFJN-DvyVA588.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-3SOmFBFw.js → wardley-L42UT6IY-BR0suGqb.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DlQV_-LH.js → wardleyDiagram-YWT4CUSO-CXMvBEO9.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-CVoTTRHF.js → xychartDiagram-2RQKCTM6-wM7UrTHx.js} +1 -1
- package/workframe-ui/public/index.html +23 -12
- package/workframe-ui/public/workframe-build.json +4 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CDDNM1ni.js +0 -1
- package/workframe-ui/public/assets/channel-R52LrLO3.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_9MbT10.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-VJadzJmf.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-kzOxPvtj.js +0 -1
- package/workframe-ui/public/assets/index-Bco9IDMA.css +0 -1
- package/workframe-ui/public/assets/index-Do34PZS2.js +0 -132
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-vJyhFZbx.js +0 -1
|
@@ -15,6 +15,7 @@ import uuid
|
|
|
15
15
|
import activity_feed
|
|
16
16
|
import api_errors
|
|
17
17
|
import chat_bind
|
|
18
|
+
import message_reactions
|
|
18
19
|
from email_sender import APP_BASE_URL, send_branded_invite_email
|
|
19
20
|
|
|
20
21
|
list_room_sessions = chat_bind.list_room_sessions
|
|
@@ -31,6 +32,56 @@ def _srv():
|
|
|
31
32
|
|
|
32
33
|
class WorkspaceRoutesMixin:
|
|
33
34
|
|
|
35
|
+
def _route_get_message_reactions(self, qs: dict[str, list[str]]) -> None:
|
|
36
|
+
srv = _srv()
|
|
37
|
+
user_id = str(getattr(self, "auth_user", "") or "")
|
|
38
|
+
if not user_id:
|
|
39
|
+
self._json(401, {"ok": False, "error": "no_session"})
|
|
40
|
+
return
|
|
41
|
+
conn = srv._workframe_db()
|
|
42
|
+
try:
|
|
43
|
+
payload = message_reactions.list_reactions(
|
|
44
|
+
conn,
|
|
45
|
+
qs.get("scope", [""])[0],
|
|
46
|
+
user_id,
|
|
47
|
+
srv._user_can_access_room,
|
|
48
|
+
)
|
|
49
|
+
except PermissionError:
|
|
50
|
+
self._json(403, {"ok": False, "error": "forbidden"})
|
|
51
|
+
return
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
self._json(400, {"ok": False, "error": str(exc)})
|
|
54
|
+
return
|
|
55
|
+
finally:
|
|
56
|
+
conn.close()
|
|
57
|
+
self._json(200, payload)
|
|
58
|
+
|
|
59
|
+
def _route_post_message_reactions_toggle(self, body: dict) -> None:
|
|
60
|
+
srv = _srv()
|
|
61
|
+
user_id = str(getattr(self, "auth_user", "") or "")
|
|
62
|
+
if not user_id:
|
|
63
|
+
self._json(401, {"ok": False, "error": "no_session"})
|
|
64
|
+
return
|
|
65
|
+
conn = srv._workframe_db()
|
|
66
|
+
try:
|
|
67
|
+
payload = message_reactions.toggle_reaction(
|
|
68
|
+
conn,
|
|
69
|
+
str(body.get("scope") or ""),
|
|
70
|
+
str(body.get("message_id") or ""),
|
|
71
|
+
str(body.get("emoji") or ""),
|
|
72
|
+
user_id,
|
|
73
|
+
srv._user_can_access_room,
|
|
74
|
+
)
|
|
75
|
+
except PermissionError:
|
|
76
|
+
self._json(403, {"ok": False, "error": "forbidden"})
|
|
77
|
+
return
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
self._json(400, {"ok": False, "error": str(exc)})
|
|
80
|
+
return
|
|
81
|
+
finally:
|
|
82
|
+
conn.close()
|
|
83
|
+
self._json(200, payload)
|
|
84
|
+
|
|
34
85
|
def _route_get_board(self, qs: dict[str, list[str]]) -> None:
|
|
35
86
|
srv = _srv()
|
|
36
87
|
self._json(200, srv.board_list())
|
|
@@ -452,8 +503,9 @@ class WorkspaceRoutesMixin:
|
|
|
452
503
|
return
|
|
453
504
|
sender_agent = str(body.get("sender_agent_id", "")).strip()
|
|
454
505
|
content = str(body.get("content", "")).strip()
|
|
455
|
-
content_type = str(body.get("content_type"
|
|
456
|
-
|
|
506
|
+
content_type = str(body.get("content_type") or "text").strip()
|
|
507
|
+
parent_value = body.get("parent_message_id")
|
|
508
|
+
parent_id = str(parent_value).strip() if parent_value else None
|
|
457
509
|
if not content:
|
|
458
510
|
self._json(400, {"error": "content required"})
|
|
459
511
|
return
|
|
@@ -875,6 +927,7 @@ class WorkspaceRoutesMixin:
|
|
|
875
927
|
self._json(400, {"error": str(exc)})
|
|
876
928
|
return
|
|
877
929
|
cred_id = str(payload["credential_id"])
|
|
930
|
+
srv._revoke_runtime_llm_leases(workspace_id=ws_id, provider=provider)
|
|
878
931
|
self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
|
|
879
932
|
try:
|
|
880
933
|
srv._bootstrap_model_after_llm_connect("", ws_id, provider)
|
|
@@ -923,6 +976,11 @@ class WorkspaceRoutesMixin:
|
|
|
923
976
|
if not affected:
|
|
924
977
|
self._json(404, {"error": "credential_not_found"})
|
|
925
978
|
return
|
|
979
|
+
srv._revoke_runtime_llm_leases(
|
|
980
|
+
workspace_id=ws_id,
|
|
981
|
+
provider=provider,
|
|
982
|
+
credential_binding_id=binding_id,
|
|
983
|
+
)
|
|
926
984
|
self._log_audit("credential_revoked", "credential_binding", binding_id, f"workspace={ws_id}")
|
|
927
985
|
spec = srv._catalog_provider(provider)
|
|
928
986
|
if spec and str(spec.get("category") or "") == "messaging":
|
|
@@ -10,6 +10,7 @@ import shutil
|
|
|
10
10
|
import sqlite3
|
|
11
11
|
import time
|
|
12
12
|
import uuid
|
|
13
|
+
from datetime import datetime, timezone
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
@@ -223,12 +224,19 @@ def _scrub_orphan_top_level_yaml_lines(profile: str) -> None:
|
|
|
223
224
|
return
|
|
224
225
|
out: list[str] = []
|
|
225
226
|
changed = False
|
|
227
|
+
root_list_key = ""
|
|
226
228
|
for line in lines:
|
|
227
229
|
stripped = line.lstrip()
|
|
228
230
|
indent = len(line) - len(stripped)
|
|
229
231
|
if indent == 0 and stripped.startswith("- "):
|
|
232
|
+
if root_list_key == "fallback_providers":
|
|
233
|
+
out.append(line)
|
|
234
|
+
continue
|
|
230
235
|
changed = True
|
|
231
236
|
continue
|
|
237
|
+
if indent == 0 and stripped and not stripped.startswith("#") and ":" in stripped:
|
|
238
|
+
key, _, value = stripped.partition(":")
|
|
239
|
+
root_list_key = key.strip() if not value.strip() else ""
|
|
232
240
|
out.append(line)
|
|
233
241
|
if changed:
|
|
234
242
|
try:
|
|
@@ -724,6 +732,14 @@ def _agent_label(profile: str) -> str:
|
|
|
724
732
|
|
|
725
733
|
|
|
726
734
|
def _native_display_name() -> str:
|
|
735
|
+
slug = str(_srv().NATIVE_PROFILE or "").strip()
|
|
736
|
+
if slug:
|
|
737
|
+
db_name = _agent_db_display_name(slug)
|
|
738
|
+
if db_name:
|
|
739
|
+
return db_name
|
|
740
|
+
reg_name = str(_srv()._agent_registry_row(slug).get("display_name") or "").strip()
|
|
741
|
+
if reg_name:
|
|
742
|
+
return reg_name
|
|
727
743
|
return f"{_srv().PROJECT_NAME} Agent"
|
|
728
744
|
|
|
729
745
|
|
|
@@ -1122,6 +1138,9 @@ def profile_create(
|
|
|
1122
1138
|
if lane.get("room"):
|
|
1123
1139
|
results["room"] = lane["room"]
|
|
1124
1140
|
results["steps"].extend(lane.get("steps") or [])
|
|
1141
|
+
if lane.get("ok") is False:
|
|
1142
|
+
results["ok"] = False
|
|
1143
|
+
results["error"] = str(lane.get("error") or "bootstrap_dm_lane_failed")
|
|
1125
1144
|
except Exception as exc:
|
|
1126
1145
|
results["steps"].append({"step": "bootstrap_dm_lane", "ok": False, "error": str(exc)})
|
|
1127
1146
|
results["ok"] = False
|
|
@@ -1159,15 +1178,18 @@ def profile_create(
|
|
|
1159
1178
|
if _install_child_base_artifacts(prof, display_name=child_label, role=child_role):
|
|
1160
1179
|
results["steps"].append({"step": "install_child_base", "ok": True})
|
|
1161
1180
|
|
|
1162
|
-
# 2.
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
results["steps"].append({"step": "
|
|
1169
|
-
|
|
1170
|
-
|
|
1181
|
+
# 2. A per-user DM runtime owns its gateway. The child template is identity/config
|
|
1182
|
+
# authority only, so starting a second template gateway is both redundant and a
|
|
1183
|
+
# secure-mode false failure. Standalone profile creation still provisions one.
|
|
1184
|
+
if not bootstrap_dm:
|
|
1185
|
+
try:
|
|
1186
|
+
ok, out, port = _srv()._configure_profile_api(prof)
|
|
1187
|
+
results["steps"].append({"step": "configure_api", "ok": ok, "api_port": port, "output": out.strip()})
|
|
1188
|
+
if ok:
|
|
1189
|
+
ok2, out2 = _srv()._patch_profile_gateway_run_script(prof)
|
|
1190
|
+
results["steps"].append({"step": "patch_run_script", "ok": ok2, "output": out2.strip()})
|
|
1191
|
+
except Exception as exc:
|
|
1192
|
+
results["steps"].append({"step": "configure_api", "ok": False, "error": str(exc)})
|
|
1171
1193
|
|
|
1172
1194
|
# 3–4. Template model/gateway only when not bootstrapping a per-user DM runtime (C1).
|
|
1173
1195
|
if not bootstrap_dm:
|
|
@@ -115,8 +115,14 @@ def _build_upstream_request(
|
|
|
115
115
|
"authorization",
|
|
116
116
|
"x-api-key",
|
|
117
117
|
"x-goog-api-key",
|
|
118
|
+
"accept-encoding",
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
# urllib does not transparently decode compressed upstream responses. If
|
|
122
|
+
# the caller's Accept-Encoding reaches OpenRouter, gzip bytes can be sent
|
|
123
|
+
# back as JSON after this proxy normalizes response headers. Prefer an
|
|
124
|
+
# identity response and still preserve Content-Encoding defensively below.
|
|
125
|
+
upstream_headers["Accept-Encoding"] = "identity"
|
|
120
126
|
upstream_headers.update(upstream_auth_header(provider, secret))
|
|
121
127
|
if provider == "openrouter":
|
|
122
128
|
upstream_headers.setdefault("HTTP-Referer", "https://workfra.me")
|
|
@@ -150,10 +156,16 @@ def forward_request(
|
|
|
150
156
|
with urllib.request.urlopen(req, timeout=600) as resp:
|
|
151
157
|
resp_body = resp.read()
|
|
152
158
|
out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
|
|
159
|
+
content_encoding = resp.headers.get("Content-Encoding", "").strip()
|
|
160
|
+
if content_encoding:
|
|
161
|
+
out_headers["Content-Encoding"] = content_encoding
|
|
153
162
|
return resp.status, out_headers, resp_body
|
|
154
163
|
except urllib.error.HTTPError as exc:
|
|
155
164
|
raw = exc.read()
|
|
156
165
|
out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
|
|
166
|
+
content_encoding = exc.headers.get("Content-Encoding", "").strip()
|
|
167
|
+
if content_encoding:
|
|
168
|
+
out_headers["Content-Encoding"] = content_encoding
|
|
157
169
|
return exc.code, out_headers, raw
|
|
158
170
|
|
|
159
171
|
|
|
@@ -190,6 +202,9 @@ def stream_request_to_handler(
|
|
|
190
202
|
content_type = resp.headers.get("Content-Type", "application/octet-stream")
|
|
191
203
|
handler.send_response(resp.status)
|
|
192
204
|
handler.send_header("Content-Type", content_type)
|
|
205
|
+
content_encoding = resp.headers.get("Content-Encoding", "").strip()
|
|
206
|
+
if content_encoding:
|
|
207
|
+
handler.send_header("Content-Encoding", content_encoding)
|
|
193
208
|
handler.end_headers()
|
|
194
209
|
if "text/event-stream" in content_type.lower():
|
|
195
210
|
while True:
|
|
@@ -209,6 +224,9 @@ def stream_request_to_handler(
|
|
|
209
224
|
raw = exc.read()
|
|
210
225
|
handler.send_response(exc.code)
|
|
211
226
|
handler.send_header("Content-Type", exc.headers.get("Content-Type", "application/json"))
|
|
227
|
+
content_encoding = exc.headers.get("Content-Encoding", "").strip()
|
|
228
|
+
if content_encoding:
|
|
229
|
+
handler.send_header("Content-Encoding", content_encoding)
|
|
212
230
|
handler.end_headers()
|
|
213
231
|
handler.wfile.write(raw)
|
|
214
232
|
except urllib.error.URLError as exc:
|
|
@@ -77,22 +77,31 @@ def _invoke_room_agent_mention(
|
|
|
77
77
|
# session. The triggering user's vault credential is overlaid per turn
|
|
78
78
|
# below; do not switch the room onto that user's private runtime profile.
|
|
79
79
|
hermes_slug = template_slug
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
# The Hermes agent template is the provider/model source of truth. The DB
|
|
81
|
+
# columns are a UI index and may lag a just-completed save, so never let
|
|
82
|
+
# them choose execution routing for a room mention.
|
|
83
|
+
model_block = _srv()._read_model_block(hermes_slug)
|
|
84
|
+
display_model = str(model_block.get("default") or agent_row.get("model_name") or "").strip()
|
|
85
|
+
display_provider = _srv()._llm_billing_provider(
|
|
86
|
+
hermes_slug,
|
|
87
|
+
user_id=triggered_by_user_id,
|
|
88
|
+
workspace_id=workspace_id,
|
|
89
|
+
block=model_block,
|
|
90
|
+
)
|
|
90
91
|
|
|
91
92
|
turn_id = str(uuid.uuid4())
|
|
92
93
|
segments: list[dict[str, Any]] = []
|
|
93
94
|
last_flush = 0.0
|
|
94
95
|
flush_interval = 0.05
|
|
95
96
|
run_id = str(uuid.uuid4())
|
|
97
|
+
turn_metadata = json.dumps(
|
|
98
|
+
{
|
|
99
|
+
"run_id": run_id,
|
|
100
|
+
"model": display_model,
|
|
101
|
+
"llm_provider": display_provider,
|
|
102
|
+
},
|
|
103
|
+
separators=(",", ":"),
|
|
104
|
+
)
|
|
96
105
|
session_id = ""
|
|
97
106
|
user_text = ""
|
|
98
107
|
provider = ""
|
|
@@ -152,8 +161,8 @@ def _invoke_room_agent_mention(
|
|
|
152
161
|
"""
|
|
153
162
|
INSERT INTO messages (
|
|
154
163
|
id, room_id, sender_user_id, sender_agent_id, parent_message_id,
|
|
155
|
-
content, content_type, is_edited, created_at, updated_at
|
|
156
|
-
) VALUES (
|
|
164
|
+
content, content_type, metadata, is_edited, created_at, updated_at
|
|
165
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
157
166
|
""",
|
|
158
167
|
(
|
|
159
168
|
mid,
|
|
@@ -163,6 +172,7 @@ def _invoke_room_agent_mention(
|
|
|
163
172
|
parent_message_id,
|
|
164
173
|
error,
|
|
165
174
|
"text",
|
|
175
|
+
turn_metadata,
|
|
166
176
|
0,
|
|
167
177
|
now_ts,
|
|
168
178
|
now_ts,
|
|
@@ -241,13 +251,7 @@ def _invoke_room_agent_mention(
|
|
|
241
251
|
raise ValueError("session_bootstrap_failed: could not start agent session for this room")
|
|
242
252
|
|
|
243
253
|
turn_body = profile_gateway._profile_turn_payload(hermes_slug, user_text, room_id)
|
|
244
|
-
provider =
|
|
245
|
-
if not provider:
|
|
246
|
-
provider = _srv()._llm_billing_provider(
|
|
247
|
-
hermes_slug,
|
|
248
|
-
user_id=triggered_by_user_id,
|
|
249
|
-
workspace_id=workspace_id,
|
|
250
|
-
)
|
|
254
|
+
provider = display_provider
|
|
251
255
|
if triggered_by_user_id:
|
|
252
256
|
run_ledger.ensure_schema()
|
|
253
257
|
auth_req = run_authority.mention_run_request(
|
|
@@ -396,8 +400,8 @@ def _invoke_room_agent_mention(
|
|
|
396
400
|
"""
|
|
397
401
|
INSERT INTO messages (
|
|
398
402
|
id, room_id, sender_user_id, sender_agent_id, parent_message_id,
|
|
399
|
-
content, content_type, is_edited, created_at, updated_at
|
|
400
|
-
) VALUES (
|
|
403
|
+
content, content_type, metadata, is_edited, created_at, updated_at
|
|
404
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
401
405
|
""",
|
|
402
406
|
(
|
|
403
407
|
mid,
|
|
@@ -407,6 +411,7 @@ def _invoke_room_agent_mention(
|
|
|
407
411
|
parent_message_id,
|
|
408
412
|
assistant,
|
|
409
413
|
"text",
|
|
414
|
+
turn_metadata,
|
|
410
415
|
0,
|
|
411
416
|
now_ts,
|
|
412
417
|
now_ts,
|
|
@@ -427,7 +432,7 @@ def _invoke_room_agent_mention(
|
|
|
427
432
|
session_id,
|
|
428
433
|
"completed",
|
|
429
434
|
provider,
|
|
430
|
-
|
|
435
|
+
display_model or "default",
|
|
431
436
|
error=None,
|
|
432
437
|
)
|
|
433
438
|
lane_bindings.chat_dispatch(
|
|
@@ -459,8 +464,8 @@ def _invoke_room_agent_mention(
|
|
|
459
464
|
triggered_by_user_id,
|
|
460
465
|
session_id,
|
|
461
466
|
"failed",
|
|
462
|
-
|
|
463
|
-
|
|
467
|
+
display_provider or "openrouter",
|
|
468
|
+
display_model or "default",
|
|
464
469
|
error=err_text,
|
|
465
470
|
)
|
|
466
471
|
except Exception: # noqa: BLE001
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Authenticated message reactions shared by room and agent-DM chat surfaces."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sqlite3
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
RoomAccessCheck = Callable[[sqlite3.Connection, str, str], bool]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resolved_scope(
|
|
13
|
+
conn: sqlite3.Connection,
|
|
14
|
+
scope: str,
|
|
15
|
+
user_id: str,
|
|
16
|
+
can_access_room: RoomAccessCheck,
|
|
17
|
+
) -> tuple[str, str | None]:
|
|
18
|
+
raw = str(scope or "").strip()
|
|
19
|
+
if not raw or len(raw) > 512:
|
|
20
|
+
raise ValueError("invalid_scope")
|
|
21
|
+
if raw.startswith("room:"):
|
|
22
|
+
room_id = raw[5:].strip()
|
|
23
|
+
if not room_id or not can_access_room(conn, room_id, user_id):
|
|
24
|
+
raise PermissionError("forbidden")
|
|
25
|
+
return f"room:{room_id}", room_id
|
|
26
|
+
if raw.startswith("dm:"):
|
|
27
|
+
dm_key = raw[3:].strip()
|
|
28
|
+
if not dm_key:
|
|
29
|
+
raise ValueError("invalid_scope")
|
|
30
|
+
# DM session identifiers are external Hermes ids. Namespacing them by
|
|
31
|
+
# the authenticated user prevents another account from reading or
|
|
32
|
+
# mutating a user's private interaction metadata.
|
|
33
|
+
return f"dm:{user_id}:{dm_key}", None
|
|
34
|
+
raise ValueError("invalid_scope")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _reaction_rows(conn: sqlite3.Connection, scope_key: str, user_id: str) -> list[dict[str, Any]]:
|
|
38
|
+
rows = conn.execute(
|
|
39
|
+
"""
|
|
40
|
+
SELECT mr.message_id, mr.emoji, mr.user_id, mr.created_at,
|
|
41
|
+
u.display_name, u.avatar_url
|
|
42
|
+
FROM message_reactions AS mr
|
|
43
|
+
LEFT JOIN users AS u ON u.id = mr.user_id
|
|
44
|
+
WHERE mr.scope_key = ?
|
|
45
|
+
ORDER BY mr.created_at, mr.emoji, mr.user_id
|
|
46
|
+
""",
|
|
47
|
+
(scope_key,),
|
|
48
|
+
).fetchall()
|
|
49
|
+
grouped: dict[tuple[str, str], dict[str, Any]] = {}
|
|
50
|
+
for row in rows:
|
|
51
|
+
message_id = str(row["message_id"])
|
|
52
|
+
emoji = str(row["emoji"])
|
|
53
|
+
key = (message_id, emoji)
|
|
54
|
+
reaction = grouped.setdefault(
|
|
55
|
+
key,
|
|
56
|
+
{
|
|
57
|
+
"message_id": message_id,
|
|
58
|
+
"emoji": emoji,
|
|
59
|
+
"count": 0,
|
|
60
|
+
"reacted": False,
|
|
61
|
+
"reactors": [],
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
reactor_user_id = str(row["user_id"])
|
|
65
|
+
reaction["count"] += 1
|
|
66
|
+
reaction["reacted"] = reaction["reacted"] or reactor_user_id == user_id
|
|
67
|
+
reaction["reactors"].append(
|
|
68
|
+
{
|
|
69
|
+
"user_id": reactor_user_id,
|
|
70
|
+
"display_name": str(row["display_name"] or "Member"),
|
|
71
|
+
"avatar_url": row["avatar_url"],
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return list(grouped.values())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_reactions(
|
|
78
|
+
conn: sqlite3.Connection,
|
|
79
|
+
scope: str,
|
|
80
|
+
user_id: str,
|
|
81
|
+
can_access_room: RoomAccessCheck,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
scope_key, _ = _resolved_scope(conn, scope, user_id, can_access_room)
|
|
84
|
+
return {"ok": True, "reactions": _reaction_rows(conn, scope_key, user_id)}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def toggle_reaction(
|
|
88
|
+
conn: sqlite3.Connection,
|
|
89
|
+
scope: str,
|
|
90
|
+
message_id: str,
|
|
91
|
+
emoji: str,
|
|
92
|
+
user_id: str,
|
|
93
|
+
can_access_room: RoomAccessCheck,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
clean_message_id = str(message_id or "").strip()
|
|
96
|
+
clean_emoji = str(emoji or "").strip()
|
|
97
|
+
if not clean_message_id or len(clean_message_id) > 512:
|
|
98
|
+
raise ValueError("invalid_message_id")
|
|
99
|
+
if not clean_emoji or len(clean_emoji) > 24:
|
|
100
|
+
raise ValueError("invalid_emoji")
|
|
101
|
+
|
|
102
|
+
scope_key, room_id = _resolved_scope(conn, scope, user_id, can_access_room)
|
|
103
|
+
if room_id:
|
|
104
|
+
exists = conn.execute(
|
|
105
|
+
"SELECT 1 FROM messages WHERE id = ? AND room_id = ? AND deleted_at IS NULL",
|
|
106
|
+
(clean_message_id, room_id),
|
|
107
|
+
).fetchone()
|
|
108
|
+
if not exists:
|
|
109
|
+
raise ValueError("message_not_found")
|
|
110
|
+
|
|
111
|
+
existing = conn.execute(
|
|
112
|
+
"""
|
|
113
|
+
SELECT 1 FROM message_reactions
|
|
114
|
+
WHERE scope_key = ? AND message_id = ? AND user_id = ? AND emoji = ?
|
|
115
|
+
""",
|
|
116
|
+
(scope_key, clean_message_id, user_id, clean_emoji),
|
|
117
|
+
).fetchone()
|
|
118
|
+
if existing:
|
|
119
|
+
conn.execute(
|
|
120
|
+
"""
|
|
121
|
+
DELETE FROM message_reactions
|
|
122
|
+
WHERE scope_key = ? AND message_id = ? AND user_id = ? AND emoji = ?
|
|
123
|
+
""",
|
|
124
|
+
(scope_key, clean_message_id, user_id, clean_emoji),
|
|
125
|
+
)
|
|
126
|
+
reacted = False
|
|
127
|
+
else:
|
|
128
|
+
conn.execute(
|
|
129
|
+
"""
|
|
130
|
+
INSERT INTO message_reactions (scope_key, message_id, user_id, emoji, created_at)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?)
|
|
132
|
+
""",
|
|
133
|
+
(scope_key, clean_message_id, user_id, clean_emoji, str(int(time.time()))),
|
|
134
|
+
)
|
|
135
|
+
reacted = True
|
|
136
|
+
conn.commit()
|
|
137
|
+
|
|
138
|
+
rows = [row for row in _reaction_rows(conn, scope_key, user_id) if row["message_id"] == clean_message_id]
|
|
139
|
+
return {"ok": True, "reacted": reacted, "message_id": clean_message_id, "reactions": rows}
|