create-workframe 0.1.12 → 0.1.14
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 +2 -2
- package/bin/workframe.js +130 -2
- package/docs/security.md +2 -2
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +9 -0
- package/scripts/sync-canonical-to-package.mjs +1 -0
- package/scripts/verify-public-deploy.sh +65 -3
- package/workframe-api/action_proxy.py +23 -17
- package/workframe-api/activity_feed.py +798 -0
- package/workframe-api/api_meta.py +157 -0
- package/workframe-api/auth_gate.py +566 -0
- package/workframe-api/avatar_registry.py +343 -0
- package/workframe-api/broker_audit.py +164 -0
- package/workframe-api/cell_authority.py +231 -0
- package/workframe-api/chat_bind.py +319 -0
- package/workframe-api/chat_sessions.py +509 -0
- package/workframe-api/chat_stream.py +694 -0
- package/workframe-api/cleanup_dogfood_smoke.py +252 -0
- package/workframe-api/credential_broker.py +162 -0
- package/workframe-api/credential_resolve.py +202 -0
- package/workframe-api/credential_store.py +245 -0
- package/workframe-api/crew_registry.py +250 -0
- package/workframe-api/db_schema.py +755 -0
- package/workframe-api/docker_gateway.py +166 -0
- package/workframe-api/doctor_runtime.py +109 -0
- package/workframe-api/domain/__init__.py +47 -0
- package/workframe-api/domain/entities.py +252 -0
- package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
- package/workframe-api/egress_policy.py +42 -0
- package/workframe-api/handler_modules/__init__.py +17 -0
- package/workframe-api/handler_modules/handler_admin.py +542 -0
- package/workframe-api/handler_modules/handler_auth.py +512 -0
- package/workframe-api/handler_modules/handler_chat.py +641 -0
- package/workframe-api/handler_modules/handler_install.py +178 -0
- package/workframe-api/handler_modules/handler_provider.py +325 -0
- package/workframe-api/handler_modules/handler_workspace.py +1420 -0
- package/workframe-api/health_monitor.py +80 -0
- package/workframe-api/hermes_admin.py +756 -0
- package/workframe-api/hermes_profiles.py +1328 -0
- package/workframe-api/install_api.py +123 -0
- package/workframe-api/kanban_cron.py +473 -0
- package/workframe-api/lane_bindings.py +423 -0
- package/workframe-api/llm_proxy.py +17 -43
- package/workframe-api/mention_helpers.py +180 -0
- package/workframe-api/mention_invoke.py +431 -0
- package/workframe-api/model_surface.py +1139 -0
- package/workframe-api/oauth_pending.py +85 -0
- package/workframe-api/oauth_redirect.py +381 -0
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_api_lifecycle.py +66 -0
- package/workframe-api/profile_gateway.py +436 -0
- package/workframe-api/provider_bindings.py +673 -0
- package/workframe-api/provider_bootstrap.py +347 -0
- package/workframe-api/provider_catalog.py +180 -0
- package/workframe-api/rooms.py +1613 -0
- package/workframe-api/route_registry.py +331 -191
- package/workframe-api/run-typecheck.mjs +2 -1
- package/workframe-api/run_authority.py +287 -0
- package/workframe-api/run_ledger.py +470 -0
- package/workframe-api/run_surface_wiring.py +344 -0
- package/workframe-api/runtime_cohort.py +752 -0
- package/workframe-api/runtime_tokens.py +127 -0
- package/workframe-api/server.py +1453 -19513
- package/workframe-api/snapshot_feed.py +49 -0
- package/workframe-api/stack_config.py +33 -1
- package/workframe-api/supervisor_client.py +154 -0
- package/workframe-api/test_api_meta_build_stamp.py +34 -0
- package/workframe-api/test_cell_authority.py +79 -0
- package/workframe-api/test_chat_bind.py +48 -0
- package/workframe-api/test_credential_lease_matrix.py +171 -0
- package/workframe-api/test_credential_resolve.py +51 -0
- package/workframe-api/test_credential_store.py +27 -0
- package/workframe-api/test_dogfood_flows.py +346 -0
- package/workframe-api/test_domain_entities.py +152 -0
- package/workframe-api/test_exception_hygiene.py +12 -0
- package/workframe-api/test_hermes_admin.py +72 -0
- package/workframe-api/test_install_wizard_resume.py +78 -0
- package/workframe-api/test_local_bootstrap.py +67 -0
- package/workframe-api/test_mention_helpers.py +35 -0
- package/workframe-api/test_mention_invoke.py +26 -0
- package/workframe-api/test_model_surface_consistency.py +1 -0
- package/workframe-api/test_oauth_pending.py +41 -0
- package/workframe-api/test_provider_bindings.py +52 -0
- package/workframe-api/test_provider_catalog.py +28 -0
- package/workframe-api/test_route_registry.py +171 -35
- package/workframe-api/test_run_authority.py +112 -0
- package/workframe-api/test_run_ledger.py +157 -0
- package/workframe-api/test_run_surface_wiring.py +130 -0
- package/workframe-api/test_runtime_cohort.py +85 -0
- package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
- package/workframe-api/test_server_reexports.py +99 -0
- package/workframe-api/test_stack_config_install_smtp.py +7 -0
- package/workframe-api/turn_credentials.py +28 -13
- package/workframe-api/turn_overlay.py +715 -0
- package/workframe-api/updates.py +16 -1
- package/workframe-api/user_prefs.py +387 -0
- package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
- package/workframe-api/workspace_bootstrap.py +536 -0
- package/workframe-api/workspace_files.py +344 -0
- package/workframe-api/workspace_messaging.py +206 -0
- package/workframe-api/zk_auth.py +3 -2
- package/workframe-supervisor/server.py +10 -1
- package/workframe-supervisor/test_supervisor_negative.py +75 -0
- package/workframe-ui/public/assets/{arc-B0OFRGmJ.js → arc-BT9iZUSd.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-m1aPUQ_r.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-CaEDd3np.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-DHJhMrIS.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-Cdyz9hIV.js} +1 -1
- package/workframe-ui/public/assets/channel-3M69ekE5.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-BbZx76LX.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DcSxjTIP.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-B7F-kTY1.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-BwwEiRTK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-DAi_NksX.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-BIaF4XCN.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-DjYSk1dG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-_twnyfgV.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CkNmdy5s.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CjLqd-gl.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-_6oNqFjj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-CeO9Am_D.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-sRM4YeIe.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-CH8tMb21.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-DAI1bsII.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-B2e-5a2G.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BMl9ed8P.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-C08HH6BU.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-DM2-0q37.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BFcOE4Aq.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BFcOE4Aq.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-BnbbewLv.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-C4zmB1Vt.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-n-BJmsiC.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-BzQGQnZi.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-Cx_De-wS.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-CIOI3Lok.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-DkRuPLWY.js} +1 -1
- package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-XhCT1Q-V.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-C3GwhDU1.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DIVBL6CF.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-CLWH8z2e.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D4h66Vy3.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-IW5F-Q6c.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-BjNIbmRM.js} +1 -1
- package/workframe-ui/public/assets/index-DL2vJP4L.css +1 -0
- package/workframe-ui/public/assets/index-ElS_D59P.js +130 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CcJspM79.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-DP4xR9jt.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-D4cZb21V.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-DsLuHkIJ.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Wu65nOsR.js} +1 -1
- package/workframe-ui/public/assets/{line-CgQsmU35.js → line-ByBDzRpy.js} +1 -1
- package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-DzXi2vnq.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-CaB-1Ckn.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-D7HTHC17.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BfKsC9aj.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-ClpU98TO.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-KeOyY_-u.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-CUpjnzbs.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-CJdPTszF.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-C9BpandZ.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-CHDZPwry.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-CMJJh91s.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-BsV4GppP.js} +1 -1
- package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-WHks82Up.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-eSX9P8z0.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-FzMK4PkM.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Dg1UkhGo.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-B7yGsBuj.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-j_G2oyQ_.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-BRrT0jqC.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-DEdUqez2.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-C-bBK9Bb.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-DhH5xuR2.js} +1 -1
- package/workframe-ui/public/index.html +11 -7
- package/workframe-ui/public/workframe-build.json +5 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +0 -1
- package/workframe-ui/public/assets/channel-fNHbDpV4.js +0 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DMfdMUwz.js +0 -2
- package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +0 -1
- package/workframe-ui/public/assets/index-8CuZDEIG.css +0 -1
- package/workframe-ui/public/assets/index-xS9lFekI.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +0 -1
package/workframe-api/updates.py
CHANGED
|
@@ -11,6 +11,8 @@ import urllib.request
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
import cell_authority
|
|
15
|
+
|
|
14
16
|
HERMES_IMAGE = os.environ.get("WORKFRAME_HERMES_IMAGE", "nousresearch/hermes-agent")
|
|
15
17
|
HERMES_TAG = os.environ.get("WORKFRAME_HERMES_TAG", "latest")
|
|
16
18
|
NPM_PACKAGE = os.environ.get("WORKFRAME_NPM_PACKAGE", "create-workframe")
|
|
@@ -542,9 +544,22 @@ def _run_apply_scripts(target: str, env: dict[str, str]) -> dict[str, Any]:
|
|
|
542
544
|
return {"ok": True, "target": target, "log": "\n".join(logs)[-12000:]}
|
|
543
545
|
|
|
544
546
|
|
|
545
|
-
def apply_update(target: str) -> dict[str, Any]:
|
|
547
|
+
def apply_update(target: str, *, user_ack: bool = False) -> dict[str, Any]:
|
|
546
548
|
if not _admin_stack_updates_enabled():
|
|
547
549
|
raise ValueError("admin_updates_disabled")
|
|
550
|
+
open_decision = cell_authority.evaluate_open(_project_root())
|
|
551
|
+
if open_decision.decision == "deny":
|
|
552
|
+
raise ValueError(open_decision.reason or "cell_open_denied")
|
|
553
|
+
update_decision = cell_authority.evaluate_update(
|
|
554
|
+
_project_root(),
|
|
555
|
+
open_decision=open_decision,
|
|
556
|
+
user_ack=user_ack,
|
|
557
|
+
)
|
|
558
|
+
if update_decision.decision == "deny":
|
|
559
|
+
raise ValueError(update_decision.reason or "cell_update_denied")
|
|
560
|
+
if user_ack and update_decision.decision != "allow":
|
|
561
|
+
raise ValueError(update_decision.reason or "cell_update_denied")
|
|
562
|
+
# ponytail: without user_ack, needs_user_action does not block apply until UI sends ack
|
|
548
563
|
target = str(target or "all").strip().lower()
|
|
549
564
|
if target not in {"hermes", "workframe", "all"}:
|
|
550
565
|
raise ValueError("invalid_update_target")
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""User preferences, profile, and onboarding payloads (WF-032)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import sqlite3
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import zk_auth as _zk
|
|
11
|
+
|
|
12
|
+
from auth_gate import OWNER_ADMIN_ROLES
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _srv():
|
|
16
|
+
import server as srv
|
|
17
|
+
|
|
18
|
+
return srv
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_user_platform_ids(value: Any) -> dict[str, str]:
|
|
22
|
+
if value is None:
|
|
23
|
+
return {}
|
|
24
|
+
if not isinstance(value, dict):
|
|
25
|
+
raise ValueError("platform_ids must be an object")
|
|
26
|
+
allowed = frozenset({"discord", "telegram", "slack"})
|
|
27
|
+
out: dict[str, str] = {}
|
|
28
|
+
for raw_key, raw_val in value.items():
|
|
29
|
+
key = str(raw_key or "").strip().lower()
|
|
30
|
+
if key not in allowed:
|
|
31
|
+
raise ValueError(f"unsupported platform_id: {raw_key}")
|
|
32
|
+
val = str(raw_val or "").strip()
|
|
33
|
+
if val:
|
|
34
|
+
out[key] = val
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def user_payload(row: sqlite3.Row) -> dict[str, Any]:
|
|
39
|
+
payload = {
|
|
40
|
+
"id": row["id"],
|
|
41
|
+
"email": row["email"],
|
|
42
|
+
"display_name": row["display_name"],
|
|
43
|
+
"avatar_url": row["avatar_url"],
|
|
44
|
+
"role": row["role"],
|
|
45
|
+
"status": row["status"],
|
|
46
|
+
"created_at": row["created_at"],
|
|
47
|
+
"updated_at": row["updated_at"],
|
|
48
|
+
}
|
|
49
|
+
keys = set(row.keys())
|
|
50
|
+
if "current_workspace_id" in keys:
|
|
51
|
+
payload["current_workspace_id"] = row["current_workspace_id"]
|
|
52
|
+
if "platform_ids" in keys:
|
|
53
|
+
raw = row["platform_ids"] or "{}"
|
|
54
|
+
try:
|
|
55
|
+
platform_ids = json.loads(raw) if isinstance(raw, str) else raw
|
|
56
|
+
except (TypeError, json.JSONDecodeError):
|
|
57
|
+
platform_ids = {}
|
|
58
|
+
payload["platform_ids"] = platform_ids if isinstance(platform_ids, dict) else {}
|
|
59
|
+
return payload
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def display_name_from_email(email: str) -> str:
|
|
63
|
+
normalized = str(email or "").strip().lower()
|
|
64
|
+
if not normalized or "@" not in normalized:
|
|
65
|
+
return ""
|
|
66
|
+
local_part = normalized.split("@", 1)[0]
|
|
67
|
+
words = [part for part in re.split(r"[._\-\s]+", local_part) if part]
|
|
68
|
+
if not words:
|
|
69
|
+
return ""
|
|
70
|
+
return " ".join(word[:1].upper() + word[1:] for word in words)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def workspace_membership_payload(row: sqlite3.Row) -> dict[str, Any]:
|
|
74
|
+
return {
|
|
75
|
+
"id": row["id"],
|
|
76
|
+
"workspace_id": row["workspace_id"],
|
|
77
|
+
"workspace_slug": row["slug"],
|
|
78
|
+
"workspace_display_name": row["display_name"],
|
|
79
|
+
"role": row["role"],
|
|
80
|
+
"status": row["status"],
|
|
81
|
+
"created_at": row["membership_created_at"],
|
|
82
|
+
"updated_at": row["membership_updated_at"],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_workframe_user(user_id: str) -> dict[str, Any] | None:
|
|
87
|
+
try:
|
|
88
|
+
db = _srv()._workframe_db()
|
|
89
|
+
row = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
90
|
+
db.close()
|
|
91
|
+
if not row:
|
|
92
|
+
return None
|
|
93
|
+
return user_payload(row)
|
|
94
|
+
except Exception:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def merge_user_platform_ids(user_id: str, patch: dict[str, str]) -> None:
|
|
99
|
+
user_id = str(user_id or "").strip()
|
|
100
|
+
if not user_id or not patch:
|
|
101
|
+
return
|
|
102
|
+
current: dict[str, str] = {}
|
|
103
|
+
user = get_workframe_user(user_id)
|
|
104
|
+
if user and isinstance(user.get("platform_ids"), dict):
|
|
105
|
+
current = {str(k): str(v) for k, v in user["platform_ids"].items() if str(v or "").strip()}
|
|
106
|
+
merged = {**current, **{str(k): str(v) for k, v in patch.items() if str(v or "").strip()}}
|
|
107
|
+
apply_me_profile_updates(user_id, {"platform_ids": merged})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def read_user_llm_prefs(user_id: str) -> tuple[str, list[dict[str, str]]]:
|
|
111
|
+
user = get_workframe_user(str(user_id or "").strip())
|
|
112
|
+
pids = user.get("platform_ids") if user and isinstance(user.get("platform_ids"), dict) else {}
|
|
113
|
+
primary = str(pids.get("llm_primary") or "").strip()
|
|
114
|
+
chain_raw = pids.get("llm_fallback_chain")
|
|
115
|
+
chain: list[dict[str, str]] = []
|
|
116
|
+
if isinstance(chain_raw, str) and chain_raw.strip():
|
|
117
|
+
try:
|
|
118
|
+
parsed = json.loads(chain_raw)
|
|
119
|
+
if isinstance(parsed, list):
|
|
120
|
+
for entry in parsed:
|
|
121
|
+
if isinstance(entry, dict):
|
|
122
|
+
prov = str(entry.get("provider") or "").strip()
|
|
123
|
+
model = str(entry.get("model") or "").strip()
|
|
124
|
+
if prov and model:
|
|
125
|
+
chain.append({"provider": prov, "model": model})
|
|
126
|
+
except json.JSONDecodeError:
|
|
127
|
+
pass
|
|
128
|
+
return primary, chain
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def write_user_llm_prefs(
|
|
132
|
+
user_id: str,
|
|
133
|
+
*,
|
|
134
|
+
primary: str = "",
|
|
135
|
+
fallback_chain: list[dict[str, str]] | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
user_id = str(user_id or "").strip()
|
|
138
|
+
if not user_id:
|
|
139
|
+
return
|
|
140
|
+
patch: dict[str, str] = {}
|
|
141
|
+
if primary:
|
|
142
|
+
patch["llm_primary"] = primary.strip()
|
|
143
|
+
if fallback_chain is not None:
|
|
144
|
+
patch["llm_fallback_chain"] = json.dumps(fallback_chain, separators=(",", ":"))
|
|
145
|
+
if patch:
|
|
146
|
+
merge_user_platform_ids(user_id, patch)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def validate_me_profile_updates(updates: dict[str, Any]) -> None:
|
|
150
|
+
avatar = updates.get("avatar_url")
|
|
151
|
+
if avatar is None:
|
|
152
|
+
return
|
|
153
|
+
text = str(avatar).strip()
|
|
154
|
+
if not text:
|
|
155
|
+
return
|
|
156
|
+
if text.startswith("data:") and len(text) > 380_000:
|
|
157
|
+
raise ValueError("Avatar image is too large. Use an image under 256 KB.")
|
|
158
|
+
if len(text) > 400_000:
|
|
159
|
+
raise ValueError("Avatar image is too large. Use an image under 256 KB.")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def apply_me_profile_updates(user_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
163
|
+
user_id = str(user_id or "").strip()
|
|
164
|
+
allowed = {"display_name", "avatar_url", "tagline", "bio"}
|
|
165
|
+
updates = {k: v for k, v in body.items() if k in allowed}
|
|
166
|
+
validate_me_profile_updates(updates)
|
|
167
|
+
if "avatar_url" in updates:
|
|
168
|
+
updates["avatar_url"] = _srv()._normalize_user_avatar_url(str(updates.get("avatar_url") or ""))
|
|
169
|
+
profile = _zk.update_profile(user_id, updates) if updates else _zk.get_profile(user_id)
|
|
170
|
+
if "platform_ids" in body:
|
|
171
|
+
platform_ids = parse_user_platform_ids(body.get("platform_ids"))
|
|
172
|
+
conn = _srv()._workframe_db()
|
|
173
|
+
try:
|
|
174
|
+
now_ts = str(int(time.time()))
|
|
175
|
+
conn.execute(
|
|
176
|
+
"UPDATE users SET platform_ids = ?, updated_at = ? WHERE id = ?",
|
|
177
|
+
(json.dumps(platform_ids, sort_keys=True), now_ts, user_id),
|
|
178
|
+
)
|
|
179
|
+
conn.commit()
|
|
180
|
+
workspaces = _srv()._get_user_workspaces(user_id)
|
|
181
|
+
current = _srv()._resolve_current_workspace(user_id, workspaces)
|
|
182
|
+
if current and str(current.get("id") or ""):
|
|
183
|
+
sync_result = _srv()._sync_workspace_messaging_gateway(str(current["id"]))
|
|
184
|
+
if not sync_result.get("ok"):
|
|
185
|
+
raise ValueError(str(sync_result.get("error") or "messaging_sync_failed"))
|
|
186
|
+
finally:
|
|
187
|
+
conn.close()
|
|
188
|
+
sync_workframe_user_profile(user_id, updates)
|
|
189
|
+
return profile or {}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def sync_workframe_user_profile(user_id: str, fields: dict[str, Any]) -> None:
|
|
193
|
+
user_id = str(user_id or "").strip()
|
|
194
|
+
if not user_id or not fields:
|
|
195
|
+
return
|
|
196
|
+
conn = _srv()._workframe_db()
|
|
197
|
+
try:
|
|
198
|
+
row = conn.execute("SELECT id FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
199
|
+
if not row:
|
|
200
|
+
return
|
|
201
|
+
sets: list[str] = []
|
|
202
|
+
vals: list[Any] = []
|
|
203
|
+
if "avatar_url" in fields:
|
|
204
|
+
sets.append("avatar_url = ?")
|
|
205
|
+
vals.append(fields["avatar_url"])
|
|
206
|
+
if "display_name" in fields:
|
|
207
|
+
sets.append("display_name = ?")
|
|
208
|
+
vals.append(fields["display_name"])
|
|
209
|
+
if "platform_ids" in fields:
|
|
210
|
+
sets.append("platform_ids = ?")
|
|
211
|
+
vals.append(json.dumps(fields["platform_ids"], sort_keys=True))
|
|
212
|
+
if not sets:
|
|
213
|
+
return
|
|
214
|
+
sets.append("updated_at = ?")
|
|
215
|
+
vals.append(str(int(time.time())))
|
|
216
|
+
vals.append(user_id)
|
|
217
|
+
conn.execute(f"UPDATE users SET {', '.join(sets)} WHERE id = ?", vals)
|
|
218
|
+
conn.commit()
|
|
219
|
+
finally:
|
|
220
|
+
conn.close()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def current_user_me(user_id: str) -> dict[str, Any]:
|
|
224
|
+
if not user_id:
|
|
225
|
+
return {"ok": False, "error": "no_authenticated_user"}
|
|
226
|
+
try:
|
|
227
|
+
conn = _srv()._workframe_db()
|
|
228
|
+
except sqlite3.Error as exc:
|
|
229
|
+
return {"ok": False, "error": f"workframe_db_unavailable: {exc}"}
|
|
230
|
+
try:
|
|
231
|
+
user = conn.execute(
|
|
232
|
+
"""
|
|
233
|
+
SELECT id, email, display_name, avatar_url, role, status, created_at, updated_at
|
|
234
|
+
FROM users
|
|
235
|
+
WHERE id = ? AND deleted_at IS NULL AND status = 'active'
|
|
236
|
+
""",
|
|
237
|
+
(user_id,),
|
|
238
|
+
).fetchone()
|
|
239
|
+
if not user:
|
|
240
|
+
return {"ok": False, "error": "user_not_found", "user_id": user_id}
|
|
241
|
+
memberships = conn.execute(
|
|
242
|
+
"""
|
|
243
|
+
SELECT DISTINCT
|
|
244
|
+
wm.id,
|
|
245
|
+
wm.workspace_id,
|
|
246
|
+
wm.role,
|
|
247
|
+
wm.status,
|
|
248
|
+
wm.created_at AS membership_created_at,
|
|
249
|
+
wm.updated_at AS membership_updated_at,
|
|
250
|
+
w.slug,
|
|
251
|
+
w.display_name
|
|
252
|
+
FROM workspace_memberships wm
|
|
253
|
+
JOIN workspaces w ON w.id = wm.workspace_id
|
|
254
|
+
WHERE wm.user_id = ?
|
|
255
|
+
AND wm.deleted_at IS NULL
|
|
256
|
+
AND wm.status = 'active'
|
|
257
|
+
AND w.deleted_at IS NULL
|
|
258
|
+
AND w.status = 'active'
|
|
259
|
+
ORDER BY w.slug, w.display_name
|
|
260
|
+
""",
|
|
261
|
+
(user_id,),
|
|
262
|
+
).fetchall()
|
|
263
|
+
return {
|
|
264
|
+
"ok": True,
|
|
265
|
+
"user": user_payload(user),
|
|
266
|
+
"workspace_memberships": [workspace_membership_payload(row) for row in memberships],
|
|
267
|
+
}
|
|
268
|
+
except sqlite3.Error as exc:
|
|
269
|
+
return {"ok": False, "error": f"current_user_lookup_failed: {exc}"}
|
|
270
|
+
finally:
|
|
271
|
+
conn.close()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def onboarding_payload(user_id: str) -> dict[str, Any]:
|
|
275
|
+
user_id = str(user_id or "").strip()
|
|
276
|
+
workspaces = _srv()._get_user_workspaces(user_id)
|
|
277
|
+
ws = next((w for w in workspaces if w.get("slug") == "default"), None) or (workspaces[0] if workspaces else None)
|
|
278
|
+
if not ws:
|
|
279
|
+
return {"ok": True, "complete": False, "step": "workspace", "credential_mode": "byok"}
|
|
280
|
+
ws_id = str(ws.get("id") or "")
|
|
281
|
+
role = str(ws.get("role") or "")
|
|
282
|
+
settings: dict[str, Any] = {}
|
|
283
|
+
try:
|
|
284
|
+
conn = _srv()._workframe_db()
|
|
285
|
+
row = conn.execute(
|
|
286
|
+
"SELECT settings_json FROM workspaces WHERE id = ? AND deleted_at IS NULL",
|
|
287
|
+
(ws_id,),
|
|
288
|
+
).fetchone()
|
|
289
|
+
conn.close()
|
|
290
|
+
if row:
|
|
291
|
+
settings = _srv()._parse_workspace_settings(row)
|
|
292
|
+
except sqlite3.Error:
|
|
293
|
+
pass
|
|
294
|
+
credential_mode = str(settings.get("credential_mode") or "byok").strip() or "byok"
|
|
295
|
+
is_admin = role in OWNER_ADMIN_ROLES
|
|
296
|
+
install_done = _srv()._install_complete()
|
|
297
|
+
integrations_done = bool(settings.get("admin_integrations_done")) or not is_admin or install_done
|
|
298
|
+
admin_done = bool(settings.get("admin_onboarding_done")) or not is_admin or install_done
|
|
299
|
+
user_has_llm = _srv()._user_has_llm_provider(user_id)
|
|
300
|
+
if is_admin and not integrations_done:
|
|
301
|
+
return {
|
|
302
|
+
"ok": True,
|
|
303
|
+
"complete": False,
|
|
304
|
+
"step": "admin_integrations",
|
|
305
|
+
"credential_mode": credential_mode,
|
|
306
|
+
"role": role,
|
|
307
|
+
"workspace_id": ws_id,
|
|
308
|
+
}
|
|
309
|
+
if is_admin and not admin_done:
|
|
310
|
+
return {
|
|
311
|
+
"ok": True,
|
|
312
|
+
"complete": False,
|
|
313
|
+
"step": "admin",
|
|
314
|
+
"credential_mode": credential_mode,
|
|
315
|
+
"role": role,
|
|
316
|
+
"workspace_id": ws_id,
|
|
317
|
+
}
|
|
318
|
+
if credential_mode == "workspace" and is_admin and not _srv()._workspace_has_llm_provider(ws_id):
|
|
319
|
+
return {
|
|
320
|
+
"ok": True,
|
|
321
|
+
"complete": False,
|
|
322
|
+
"step": "workspace_provider",
|
|
323
|
+
"credential_mode": credential_mode,
|
|
324
|
+
"role": role,
|
|
325
|
+
"workspace_id": ws_id,
|
|
326
|
+
}
|
|
327
|
+
if credential_mode == "byok" and not user_has_llm:
|
|
328
|
+
return {
|
|
329
|
+
"ok": True,
|
|
330
|
+
"complete": False,
|
|
331
|
+
"step": "user_provider",
|
|
332
|
+
"credential_mode": credential_mode,
|
|
333
|
+
"role": role,
|
|
334
|
+
"workspace_id": ws_id,
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
"ok": True,
|
|
338
|
+
"complete": True,
|
|
339
|
+
"step": "done",
|
|
340
|
+
"credential_mode": credential_mode,
|
|
341
|
+
"role": role,
|
|
342
|
+
"workspace_id": ws_id,
|
|
343
|
+
"has_llm_provider": user_has_llm,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def session_profile_payload(user_id: str) -> dict[str, Any] | None:
|
|
348
|
+
user_id = str(user_id or "").strip()
|
|
349
|
+
if not user_id:
|
|
350
|
+
return None
|
|
351
|
+
profile = _zk.get_profile(user_id)
|
|
352
|
+
user = get_workframe_user(user_id)
|
|
353
|
+
if not profile and not user:
|
|
354
|
+
return None
|
|
355
|
+
workspaces = _srv()._get_user_workspaces(user_id)
|
|
356
|
+
email = str((user or {}).get("email", "") or "")
|
|
357
|
+
display_name = (
|
|
358
|
+
str((profile or {}).get("display_name", "") or "").strip()
|
|
359
|
+
or str((user or {}).get("display_name", "") or "").strip()
|
|
360
|
+
or display_name_from_email(email)
|
|
361
|
+
)
|
|
362
|
+
default_workspace = next((ws for ws in workspaces if ws.get("slug") == "default"), None)
|
|
363
|
+
current_workspace = _srv()._resolve_current_workspace(user_id, workspaces)
|
|
364
|
+
wf_user = get_workframe_user(user_id) or {}
|
|
365
|
+
platform_ids = wf_user.get("platform_ids") if isinstance(wf_user.get("platform_ids"), dict) else {}
|
|
366
|
+
zk_avatar = str((profile or {}).get("avatar_url") or "").strip()
|
|
367
|
+
wf_avatar = str((user or {}).get("avatar_url") or wf_user.get("avatar_url") or "").strip()
|
|
368
|
+
avatar_url = (
|
|
369
|
+
_srv()._normalize_user_avatar_url(zk_avatar or wf_avatar) if (zk_avatar or wf_avatar) else None
|
|
370
|
+
)
|
|
371
|
+
return {
|
|
372
|
+
"ok": True,
|
|
373
|
+
"user": {
|
|
374
|
+
"user_id": user_id,
|
|
375
|
+
"id": user_id,
|
|
376
|
+
"email": email,
|
|
377
|
+
"display_name": display_name,
|
|
378
|
+
"avatar_url": avatar_url,
|
|
379
|
+
"tagline": (profile or {}).get("tagline"),
|
|
380
|
+
"bio": (profile or {}).get("bio"),
|
|
381
|
+
"platform_ids": platform_ids,
|
|
382
|
+
},
|
|
383
|
+
"workspaces": workspaces,
|
|
384
|
+
"current_workspace": current_workspace,
|
|
385
|
+
"default_workspace": default_workspace,
|
|
386
|
+
"hermes_dashboard_access": _srv()._user_can_access_hermes_dashboard(user_id),
|
|
387
|
+
}
|