create-workframe 0.1.7 → 0.1.9
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/package.json +1 -1
- package/scripts/apply-update-workframe.sh +68 -28
- package/scripts/compose-docker-host.sh +8 -2
- package/workframe-api/.dockerignore +4 -0
- package/workframe-api/package.json +1 -1
- package/workframe-api/server.py +306 -70
- package/workframe-api/stack_config.py +7 -7
- package/workframe-api/test_stack_config_install_smtp.py +5 -0
- package/workframe-api/updates.py +146 -33
- package/workframe-supervisor/Dockerfile +1 -1
- package/workframe-supervisor/server.py +16 -3
- package/workframe-ui/public/assets/{arc-Cn-lt9fT.js → arc-DjpS9a2b.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-qXoGCqfH.js → architectureDiagram-3BPJPVTR-puCKmKHV.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DoopFlWj.js → blockDiagram-GPEHLZMM-C2P0g-eH.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BRa7hfzE.js → c4Diagram-AAUBKEIU-DdvYEITH.js} +1 -1
- package/workframe-ui/public/assets/channel-D63s_yN9.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Cl33E2hh.js → chunk-2J33WTMH-Cxe8QsEF.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-BRywYZQO.js → chunk-3OPIFGDE-DPNyTwR3.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-CI3xzp-0.js → chunk-4BX2VUAB-XKG9gfRN.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-Bkf3Ye3F.js → chunk-55IACEB6-oPMTulgb.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-C5RN3hUT.js → chunk-5ZQYHXKU-BWwXvpa_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BaY0yDwt.js → chunk-727SXJPM-Cml2twO_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-VMMBaDqd.js → chunk-AQP2D5EJ-DzrRBhQu.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-DlNZ1UV3.js → chunk-BSJP7CBP-EZ7STU6h.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-tT2HyVy0.js → chunk-CSCIHK7Q-BGTQzWYw.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CyACRaUT.js → chunk-FMBD7UC4-Cm4stfRa.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-seK5WAZs.js → chunk-KSCS5N6A-exbMijjy.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-r5cyG5AH.js → chunk-L5ZTLDWV-C75kbQwN.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-SnbeVxsJ.js → chunk-LZXEDZCA-CsRdBqUB.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-zbFgEXSJ.js → chunk-ND2GUHAM-D84zyIu2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-UhZc8kOk.js → chunk-NZK2D7GU-CUNNo1bj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DHvu2iZ-.js → chunk-O5CBEL6O-DN6WEIh-.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-CCPNKbzG.js → chunk-XPW4576I-Ct5VQv2D.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-D1CLe6Y0.js → cose-bilkent-S5V4N54A-Bj4W5oMk.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DYHcGVs_.js → dagre-BM42HDAG-DAjHBkAy.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-DVUkJ-II.js → diagram-2AECGRRQ-Ch2gGFPG.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-C9Y1Acc2.js → diagram-5GNKFQAL-BzIIvgYH.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Db3zez-n.js → diagram-KO2AKTUF-Df0Q8nv6.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-BaOR5sIr.js → diagram-LMA3HP47-D-G3ekmF.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-fLBzMY5W.js → diagram-OG6HWLK6-TBHhT9f5.js} +1 -1
- package/workframe-ui/public/assets/{dist-uZYeD3o1.js → dist-CPbiDkEZ.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BoCxDSyE.js → erDiagram-TEJ5UH35-YxkUmLLV.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-BnYa3CAj.js → flowDiagram-I6XJVG4X-kfZ7-RSd.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-Dm_e_Kaa.js → ganttDiagram-6RSMTGT7-DmTx3hzR.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-CBAj8ntv.js → gitGraph-WXDBUCRP-BXa0ewp3.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-D5auJjJh.js → gitGraphDiagram-PVQCEYII-CQr2qMxU.js} +1 -1
- package/workframe-ui/public/assets/{index-auJ0bLMU.css → index-B08aShJy.css} +1 -1
- package/workframe-ui/public/assets/index-DCF3W3G_.js +129 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-BNMADxE2.js → info-J43DQDTF-t-Qlx8zv.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C8c45T4J.js → infoDiagram-5YYISTIA-CzkYofPx.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CyWuHB4e.js → ishikawaDiagram-YF4QCWOH-djK3Oxgb.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-B43pCuDT.js → journeyDiagram-JHISSGLW-CWxYXxdF.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Dyn480BI.js → kanban-definition-UN3LZRKU-D7leCAD9.js} +1 -1
- package/workframe-ui/public/assets/{line-CNtIbnmt.js → line-BSFj2OC5.js} +1 -1
- package/workframe-ui/public/assets/{linear-pmSg_4uV.js → linear-pfPwnu-o.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-BX4tnojE.js → mermaid-parser.core-Dm2L9Ib_.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-BtpBva5o.js → mermaid.core-Bevx9bif.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DcMcB_69.js → mindmap-definition-RKZ34NQL-UkjDssmP.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-KSzK9WlD.js → packet-YPE3B663-JO2ezH3_.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DLOH6Mg8.js → pie-LRSECV5Y-DdMFsfR8.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BhrX2aGk.js → pieDiagram-4H26LBE5-CEPHFBU1.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-B5dDjbTX.js → quadrantDiagram-W4KKPZXB-_RF9_86k.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-BPdjqZiu.js → radar-GUYGQ44K-Cb-YfdaA.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-21Dq5e0N.js → requirementDiagram-4Y6WPE33-DBVOxmBn.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-CiA99dbO.js → sankeyDiagram-5OEKKPKP-BTpxsGUA.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-Bwpui7_x.js → sequenceDiagram-3UESZ5HK-DHP_Bq3l.js} +1 -1
- package/workframe-ui/public/assets/{src-B3Axsi1U.js → src-CY_By-r7.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-C76qwtE8.js → stateDiagram-AJRCARHV-BVtp_LPR.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-CufInOyk.js → timeline-definition-PNZ67QCA-D38HJhop.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-C4sxh0R-.js → treeView-BLDUP644-CFCG3Sc_.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BjV72ANV.js → treemap-LRROVOQU-BeiMs_dF.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DgsdR0Rl.js → vennDiagram-CIIHVFJN-CKMeT6PW.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-DZ279G_t.js → wardley-L42UT6IY-BljDy8ph.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-Bde25kvz.js → wardleyDiagram-YWT4CUSO-DEE8D94J.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B8VnS2Pb.js → xychartDiagram-2RQKCTM6-BRaAWV_g.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CCMNXHod.js +0 -1
- package/workframe-ui/public/assets/channel-CtFV3sE3.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-B1g7l1_B.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B_qfZ6QP.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CJoeCNq6.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CJoeCNq6.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bw910jho.js +0 -1
- package/workframe-ui/public/assets/index-DHLQNAXd.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-Dn0CDWJD.js +0 -1
package/workframe-api/server.py
CHANGED
|
@@ -551,44 +551,11 @@ def _admin_write_allowed(handler: BaseHTTPRequestHandler) -> bool:
|
|
|
551
551
|
|
|
552
552
|
|
|
553
553
|
def _apply_stack_update(target: str) -> dict[str, Any]:
|
|
554
|
-
|
|
555
|
-
if target in {"workframe", "all"}:
|
|
556
|
-
try:
|
|
557
|
-
workframe_version = str(
|
|
558
|
-
stack_updates.updates_available().get("workframe", {}).get("latest") or ""
|
|
559
|
-
).strip()
|
|
560
|
-
except Exception: # noqa: BLE001
|
|
561
|
-
workframe_version = ""
|
|
562
|
-
try:
|
|
563
|
-
return stack_updates.apply_update(target)
|
|
564
|
-
except ValueError as exc:
|
|
565
|
-
err = str(exc)
|
|
566
|
-
if not _supervisor_ready():
|
|
567
|
-
raise
|
|
568
|
-
if err not in ("docker_unavailable", "admin_updates_disabled"):
|
|
569
|
-
raise
|
|
570
|
-
body: dict[str, Any] = {"target": target}
|
|
571
|
-
if workframe_version:
|
|
572
|
-
body["workframe_version"] = workframe_version
|
|
573
|
-
status, data = _supervisor_request("POST", "/v1/stack.apply", body, timeout=900.0)
|
|
574
|
-
if status >= 300 or not isinstance(data, dict) or not data.get("ok"):
|
|
575
|
-
raise ValueError(str((data or {}).get("error") or "supervisor_apply_failed"))
|
|
576
|
-
return data
|
|
554
|
+
return stack_updates.apply_update(target)
|
|
577
555
|
|
|
578
556
|
|
|
579
557
|
def _restart_stack_gateway() -> dict[str, Any]:
|
|
580
|
-
|
|
581
|
-
return stack_updates.restart_gateway()
|
|
582
|
-
except ValueError as exc:
|
|
583
|
-
# ponytail: SECURE_MODE API has no docker.sock; supervisor owns gateway restart
|
|
584
|
-
if str(exc) not in ("docker_unavailable", "admin_updates_disabled") or not _supervisor_ready():
|
|
585
|
-
raise
|
|
586
|
-
status, data = _supervisor_request(
|
|
587
|
-
"POST", "/v1/stack.apply", {"target": "gateway-restart"}, timeout=300.0,
|
|
588
|
-
)
|
|
589
|
-
if status >= 300 or not isinstance(data, dict) or not data.get("ok"):
|
|
590
|
-
raise ValueError(str((data or {}).get("error") or "supervisor_restart_failed"))
|
|
591
|
-
return data
|
|
558
|
+
return stack_updates.restart_gateway()
|
|
592
559
|
|
|
593
560
|
|
|
594
561
|
def _cors_origin_for(headers=None) -> str:
|
|
@@ -908,8 +875,13 @@ def _auth_check(handler: BaseHTTPRequestHandler) -> bool:
|
|
|
908
875
|
if method == "OPTIONS":
|
|
909
876
|
return True
|
|
910
877
|
|
|
911
|
-
# GET public routes
|
|
878
|
+
# GET public routes — still attach session user when cookie/header present
|
|
879
|
+
# (e.g. /api/hermes/models needs auth_user for per-user provider detection).
|
|
912
880
|
if method == "GET" and _is_public_get(path):
|
|
881
|
+
if sid:
|
|
882
|
+
session_info = _zk.validate_session_token(sid)
|
|
883
|
+
if session_info:
|
|
884
|
+
_apply_session_user(handler, session_info["user_id"])
|
|
913
885
|
return True
|
|
914
886
|
|
|
915
887
|
# POST auth endpoints (start/verify/logout/refresh) are public
|
|
@@ -2076,6 +2048,53 @@ def _merge_user_platform_ids(user_id: str, patch: dict[str, str]) -> None:
|
|
|
2076
2048
|
_apply_me_profile_updates(user_id, {"platform_ids": merged})
|
|
2077
2049
|
|
|
2078
2050
|
|
|
2051
|
+
def _read_user_llm_prefs(user_id: str) -> tuple[str, list[dict[str, str]]]:
|
|
2052
|
+
"""User-level default model prefs (not agent profile config)."""
|
|
2053
|
+
user = _get_workframe_user(str(user_id or "").strip())
|
|
2054
|
+
pids = user.get("platform_ids") if user and isinstance(user.get("platform_ids"), dict) else {}
|
|
2055
|
+
primary = str(pids.get("llm_primary") or "").strip()
|
|
2056
|
+
chain_raw = pids.get("llm_fallback_chain")
|
|
2057
|
+
chain: list[dict[str, str]] = []
|
|
2058
|
+
if isinstance(chain_raw, str) and chain_raw.strip():
|
|
2059
|
+
try:
|
|
2060
|
+
parsed = json.loads(chain_raw)
|
|
2061
|
+
if isinstance(parsed, list):
|
|
2062
|
+
for entry in parsed:
|
|
2063
|
+
if isinstance(entry, dict):
|
|
2064
|
+
prov = str(entry.get("provider") or "").strip()
|
|
2065
|
+
model = str(entry.get("model") or "").strip()
|
|
2066
|
+
if prov and model:
|
|
2067
|
+
chain.append({"provider": prov, "model": model})
|
|
2068
|
+
except json.JSONDecodeError:
|
|
2069
|
+
pass
|
|
2070
|
+
return primary, chain
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
def _write_user_llm_prefs(
|
|
2074
|
+
user_id: str,
|
|
2075
|
+
*,
|
|
2076
|
+
primary: str = "",
|
|
2077
|
+
fallback_chain: list[dict[str, str]] | None = None,
|
|
2078
|
+
) -> None:
|
|
2079
|
+
user_id = str(user_id or "").strip()
|
|
2080
|
+
if not user_id:
|
|
2081
|
+
return
|
|
2082
|
+
patch: dict[str, str] = {}
|
|
2083
|
+
if primary:
|
|
2084
|
+
patch["llm_primary"] = primary.strip()
|
|
2085
|
+
if fallback_chain is not None:
|
|
2086
|
+
patch["llm_fallback_chain"] = json.dumps(fallback_chain, separators=(",", ":"))
|
|
2087
|
+
if patch:
|
|
2088
|
+
_merge_user_platform_ids(user_id, patch)
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _user_llm_has_provider(user_id: str, workspace_id: str = "") -> bool:
|
|
2092
|
+
if _user_has_llm_provider(user_id):
|
|
2093
|
+
return True
|
|
2094
|
+
ws = str(workspace_id or "").strip()
|
|
2095
|
+
return bool(ws and _workspace_has_llm_provider(ws))
|
|
2096
|
+
|
|
2097
|
+
|
|
2079
2098
|
def _start_discord_oauth(user_id: str, workspace_id: str = "") -> dict[str, Any]:
|
|
2080
2099
|
cfg = platform_auth.resolved_discord_oauth()
|
|
2081
2100
|
if not cfg.get("client_id") or not cfg.get("client_secret"):
|
|
@@ -5098,6 +5117,25 @@ def bootstrap_agent_dm_lane(
|
|
|
5098
5117
|
try:
|
|
5099
5118
|
_bootstrap_profile_providers(runtime, user_id, workspace_id)
|
|
5100
5119
|
steps.append({"step": "bootstrap_providers", "ok": True})
|
|
5120
|
+
user_primary, user_chain = _read_user_llm_prefs(user_id)
|
|
5121
|
+
if user_primary:
|
|
5122
|
+
applied = hermes_model_set(runtime, user_primary, user_id, workspace_id)
|
|
5123
|
+
steps.append(
|
|
5124
|
+
{
|
|
5125
|
+
"step": "user_default_model",
|
|
5126
|
+
"ok": bool(applied.get("ok")),
|
|
5127
|
+
"model": user_primary,
|
|
5128
|
+
},
|
|
5129
|
+
)
|
|
5130
|
+
if user_chain:
|
|
5131
|
+
chained = hermes_fallback_chain_set(runtime, user_chain, user_id=user_id)
|
|
5132
|
+
steps.append(
|
|
5133
|
+
{
|
|
5134
|
+
"step": "user_default_fallbacks",
|
|
5135
|
+
"ok": bool(chained.get("ok")),
|
|
5136
|
+
"count": len(user_chain),
|
|
5137
|
+
},
|
|
5138
|
+
)
|
|
5101
5139
|
except Exception as exc:
|
|
5102
5140
|
steps.append({"step": "bootstrap_providers", "ok": False, "error": str(exc)})
|
|
5103
5141
|
|
|
@@ -6169,6 +6207,7 @@ def ensure_runtime_profile(
|
|
|
6169
6207
|
if not _profile_toolsets_ready(runtime, _chat_toolsets_for_profile(runtime)):
|
|
6170
6208
|
_ensure_profile_toolsets(runtime)
|
|
6171
6209
|
_ensure_profile_terminal_cwd(runtime)
|
|
6210
|
+
_sync_runtime_model_from_template(runtime, template)
|
|
6172
6211
|
return
|
|
6173
6212
|
_purge_runtime_profile(runtime)
|
|
6174
6213
|
_register_runtime_profile(runtime, template)
|
|
@@ -10678,6 +10717,28 @@ def _profile_model(profile: str) -> str:
|
|
|
10678
10717
|
conn.close()
|
|
10679
10718
|
|
|
10680
10719
|
|
|
10720
|
+
def _llm_attribution_for_profile(profile: str, session_model: str = "") -> tuple[str, str]:
|
|
10721
|
+
"""Resolve model id + billing provider for chat turn footers."""
|
|
10722
|
+
_ = session_model # ponytail: Hermes sessions.model is profile slug, not LLM id
|
|
10723
|
+
try:
|
|
10724
|
+
prof = resolve_hermes_profile(str(profile or "").strip())
|
|
10725
|
+
except ValueError:
|
|
10726
|
+
return "", ""
|
|
10727
|
+
block = _read_model_block(prof)
|
|
10728
|
+
model = str(block.get("default") or "").strip()
|
|
10729
|
+
billing = _billing_provider_id_from_hermes_config(str(block.get("provider") or ""))
|
|
10730
|
+
if not billing:
|
|
10731
|
+
billing = _llm_billing_provider(prof)
|
|
10732
|
+
if not billing and model:
|
|
10733
|
+
infer_llm = {
|
|
10734
|
+
str(spec["id"]).lower()
|
|
10735
|
+
for spec in PROVIDER_CONNECT_CATALOG
|
|
10736
|
+
if str(spec.get("category") or "") == "llm"
|
|
10737
|
+
}
|
|
10738
|
+
billing = _resolve_billing_provider_for_model(model, infer_llm)
|
|
10739
|
+
return model, str(billing or "").strip()
|
|
10740
|
+
|
|
10741
|
+
|
|
10681
10742
|
def _latest_session_id(profile: str) -> str:
|
|
10682
10743
|
"""Most recent session — prefer active (ended_at IS NULL), then by started_at."""
|
|
10683
10744
|
db = _profile_dir(profile) / "state.db"
|
|
@@ -10904,13 +10965,16 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
10904
10965
|
_runtime_template_slug(profile) if _is_runtime_profile_slug(profile) else profile
|
|
10905
10966
|
)
|
|
10906
10967
|
display = _profile_display_name(profile)
|
|
10968
|
+
attr_model, attr_provider = _llm_attribution_for_profile(profile)
|
|
10907
10969
|
try:
|
|
10908
10970
|
raw = conn.execute(
|
|
10909
10971
|
"""
|
|
10910
|
-
SELECT id, role, content, reasoning_content, tool_name,
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10972
|
+
SELECT m.id, m.role, m.content, m.reasoning_content, m.tool_name,
|
|
10973
|
+
m.timestamp, m.token_count, s.model AS session_model
|
|
10974
|
+
FROM messages m
|
|
10975
|
+
LEFT JOIN sessions s ON s.id = m.session_id
|
|
10976
|
+
WHERE m.role IN ('user', 'assistant', 'tool') AND m.session_id = ?
|
|
10977
|
+
ORDER BY m.timestamp ASC, m.id ASC
|
|
10914
10978
|
LIMIT 200
|
|
10915
10979
|
""",
|
|
10916
10980
|
(sid,),
|
|
@@ -10943,6 +11007,9 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
10943
11007
|
if current and current["role"] == "agent":
|
|
10944
11008
|
current["segments"].extend(segments)
|
|
10945
11009
|
current["tokens"] += tokens
|
|
11010
|
+
if attr_model:
|
|
11011
|
+
current["model"] = attr_model
|
|
11012
|
+
current["llm_provider"] = attr_provider
|
|
10946
11013
|
else:
|
|
10947
11014
|
if current:
|
|
10948
11015
|
turns.append(current)
|
|
@@ -10954,6 +11021,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
10954
11021
|
"segments": list(segments),
|
|
10955
11022
|
"timestamp": ts,
|
|
10956
11023
|
"tokens": tokens,
|
|
11024
|
+
"model": attr_model,
|
|
11025
|
+
"llm_provider": attr_provider,
|
|
10957
11026
|
}
|
|
10958
11027
|
if current:
|
|
10959
11028
|
turns.append(current)
|
|
@@ -12678,13 +12747,13 @@ def _overlay_chat_llm_env(
|
|
|
12678
12747
|
|
|
12679
12748
|
|
|
12680
12749
|
def _resolve_models_profile(profile: str) -> str:
|
|
12681
|
-
"""
|
|
12750
|
+
"""Template profile slug for model picker reads — never per-user runtime u-*."""
|
|
12682
12751
|
raw = str(profile or "").strip()
|
|
12683
12752
|
if not raw:
|
|
12684
12753
|
return _primary_profile()
|
|
12685
12754
|
slug = safe_profile_slug(raw)
|
|
12686
12755
|
if _is_runtime_profile_slug(slug):
|
|
12687
|
-
return
|
|
12756
|
+
return resolve_validated_profile(_runtime_template_slug(slug))
|
|
12688
12757
|
return resolve_validated_profile(slug)
|
|
12689
12758
|
|
|
12690
12759
|
|
|
@@ -12713,10 +12782,13 @@ def _augment_model_suggestions(
|
|
|
12713
12782
|
extra: list[dict[str, str]] = []
|
|
12714
12783
|
primary = str(block.get("default") or "").strip()
|
|
12715
12784
|
if primary and primary not in seen:
|
|
12785
|
+
bill = _billing_provider_id_from_hermes_config(str(block.get("provider") or "")) or str(
|
|
12786
|
+
block.get("provider") or "openrouter"
|
|
12787
|
+
)
|
|
12716
12788
|
extra.append(
|
|
12717
12789
|
_model_suggestion_row(
|
|
12718
12790
|
primary,
|
|
12719
|
-
provider_label=
|
|
12791
|
+
provider_label=_provider_display_label(bill),
|
|
12720
12792
|
label="Current primary",
|
|
12721
12793
|
description="Active model for this agent profile",
|
|
12722
12794
|
),
|
|
@@ -12732,10 +12804,11 @@ def _augment_model_suggestions(
|
|
|
12732
12804
|
full = model if "/" in model else f"{prov}/{model}"
|
|
12733
12805
|
if full in seen:
|
|
12734
12806
|
continue
|
|
12807
|
+
bill = _billing_provider_id_from_hermes_config(prov) or prov
|
|
12735
12808
|
extra.append(
|
|
12736
12809
|
_model_suggestion_row(
|
|
12737
12810
|
full,
|
|
12738
|
-
provider_label=
|
|
12811
|
+
provider_label=_provider_display_label(bill),
|
|
12739
12812
|
label=f"Fallback · {model}",
|
|
12740
12813
|
description="Configured in this profile's fallback chain",
|
|
12741
12814
|
),
|
|
@@ -13732,6 +13805,8 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
13732
13805
|
if _triggering_user:
|
|
13733
13806
|
_reconcile_profile_llm_for_user(prof, _triggering_user, _workspace_id)
|
|
13734
13807
|
llm_provider = _llm_billing_provider(prof, user_id=_triggering_user, workspace_id=_workspace_id)
|
|
13808
|
+
model_block = _read_model_block(prof)
|
|
13809
|
+
model_used = str(model_block.get("default") or "").strip()
|
|
13735
13810
|
llm_ready = bool(
|
|
13736
13811
|
_triggering_user
|
|
13737
13812
|
and _user_can_use_llm(_triggering_user, _workspace_id, llm_provider)
|
|
@@ -13810,8 +13885,10 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
13810
13885
|
handler.send_header("X-Workframe-Api-Port", str(lifecycle["api_port"]))
|
|
13811
13886
|
handler.end_headers()
|
|
13812
13887
|
handler.wfile.write(
|
|
13813
|
-
|
|
13814
|
-
|
|
13888
|
+
(
|
|
13889
|
+
"event: run.started\n"
|
|
13890
|
+
f"data: {json.dumps({'text': 'Contacting model...', 'model': model_used, 'llm_provider': llm_provider})}\n\n"
|
|
13891
|
+
).encode("utf-8")
|
|
13815
13892
|
)
|
|
13816
13893
|
handler.wfile.flush()
|
|
13817
13894
|
buffer = b""
|
|
@@ -15122,7 +15199,7 @@ def _set_profile_model_provider(profile: str, provider: str) -> tuple[bool, str]
|
|
|
15122
15199
|
return True, ""
|
|
15123
15200
|
|
|
15124
15201
|
|
|
15125
|
-
def
|
|
15202
|
+
def _parse_model_block_from_disk(profile: str) -> dict[str, Any]:
|
|
15126
15203
|
"""Parse the model surface from the profile's config.yaml:
|
|
15127
15204
|
`model.default`, `model.provider`, etc. (siblings of `model:`) AND
|
|
15128
15205
|
the top-level `fallback_providers:` block. Pure text scan — no
|
|
@@ -15231,6 +15308,48 @@ def _read_model_block(profile: str) -> dict[str, Any]:
|
|
|
15231
15308
|
return out
|
|
15232
15309
|
|
|
15233
15310
|
|
|
15311
|
+
def _read_model_block(profile: str) -> dict[str, Any]:
|
|
15312
|
+
"""Model surface for a profile. Runtime u-* slugs inherit default/chain from template."""
|
|
15313
|
+
raw = str(profile or "").strip()
|
|
15314
|
+
if not raw:
|
|
15315
|
+
return _parse_model_block_from_disk("")
|
|
15316
|
+
try:
|
|
15317
|
+
prof = resolve_hermes_profile(raw)
|
|
15318
|
+
except ValueError:
|
|
15319
|
+
return _parse_model_block_from_disk(raw)
|
|
15320
|
+
block = _parse_model_block_from_disk(prof)
|
|
15321
|
+
if not _is_runtime_profile_slug(prof):
|
|
15322
|
+
return block
|
|
15323
|
+
template = resolve_validated_profile(_runtime_template_slug(prof))
|
|
15324
|
+
tblock = _parse_model_block_from_disk(template)
|
|
15325
|
+
if str(tblock.get("default") or "").strip():
|
|
15326
|
+
block["default"] = tblock["default"]
|
|
15327
|
+
if tblock.get("fallback_chain"):
|
|
15328
|
+
block["fallback_chain"] = list(tblock["fallback_chain"])
|
|
15329
|
+
tpl_provider = str(tblock.get("provider") or "").strip()
|
|
15330
|
+
if tpl_provider:
|
|
15331
|
+
block["provider"] = tpl_provider
|
|
15332
|
+
return block
|
|
15333
|
+
|
|
15334
|
+
|
|
15335
|
+
def _sync_runtime_model_from_template(runtime: str, template: str) -> None:
|
|
15336
|
+
"""Keep per-user runtime Hermes config aligned with agent template model picks."""
|
|
15337
|
+
runtime_slug = safe_profile_slug(runtime)
|
|
15338
|
+
template_slug = resolve_validated_profile(template)
|
|
15339
|
+
if not _is_runtime_profile_slug(runtime_slug) or not profile_exists(runtime_slug):
|
|
15340
|
+
return
|
|
15341
|
+
tblock = _parse_model_block_from_disk(template_slug)
|
|
15342
|
+
default = str(tblock.get("default") or "").strip()
|
|
15343
|
+
if default:
|
|
15344
|
+
_set_profile_model(runtime_slug, default)
|
|
15345
|
+
chain = [e for e in (tblock.get("fallback_chain") or []) if isinstance(e, dict)]
|
|
15346
|
+
if chain:
|
|
15347
|
+
_write_fallback_chain(runtime_slug, chain)
|
|
15348
|
+
billing = _billing_provider_id_from_hermes_config(str(tblock.get("provider") or ""))
|
|
15349
|
+
if billing and default:
|
|
15350
|
+
_apply_model_for_billing_provider(runtime_slug, billing, default)
|
|
15351
|
+
|
|
15352
|
+
|
|
15234
15353
|
def _write_fallback_chain(profile: str, chain: list[dict[str, str]]) -> tuple[bool, str]:
|
|
15235
15354
|
"""Rewrite the `fallback_providers` block in config.yaml to match the
|
|
15236
15355
|
supplied chain. Preserves all other keys, comments, and ordering.
|
|
@@ -15332,24 +15451,22 @@ def _write_fallback_chain(profile: str, chain: list[dict[str, str]]) -> tuple[bo
|
|
|
15332
15451
|
return True, ""
|
|
15333
15452
|
|
|
15334
15453
|
|
|
15335
|
-
def hermes_models(
|
|
15454
|
+
def hermes_models(
|
|
15455
|
+
profile: str = "",
|
|
15456
|
+
user_id: str = "",
|
|
15457
|
+
workspace_id: str = "",
|
|
15458
|
+
*,
|
|
15459
|
+
selection_only: bool = False,
|
|
15460
|
+
) -> dict[str, Any]:
|
|
15336
15461
|
"""Return a profile's model surface: primary, fallback chain,
|
|
15337
15462
|
and curated suggestions scoped to the active provider. The catalog
|
|
15338
15463
|
is a hint list, not a hardcoded menu — the BFF accepts any
|
|
15339
15464
|
`provider/model` id and the picker always lets the user type a
|
|
15340
15465
|
custom one.
|
|
15341
15466
|
"""
|
|
15342
|
-
primary_profile = _resolve_models_profile(profile) if profile else _primary_profile()
|
|
15343
|
-
if primary_profile:
|
|
15344
|
-
_bootstrap_profile_providers(primary_profile, user_id, workspace_id)
|
|
15345
|
-
block = _read_model_block(primary_profile) if primary_profile else {
|
|
15346
|
-
"default": "", "provider": "", "base_url": "",
|
|
15347
|
-
"providers": {}, "fallback_chain": [],
|
|
15348
|
-
}
|
|
15349
|
-
active_provider = block.get("provider", "").strip()
|
|
15350
15467
|
picker_llm = _user_llm_providers_for_picker(user_id) if user_id else set()
|
|
15351
15468
|
connected = _connected_provider_names(user_id, workspace_id)
|
|
15352
|
-
has_llm =
|
|
15469
|
+
has_llm = _user_llm_has_provider(user_id, workspace_id) if user_id else any(
|
|
15353
15470
|
str(spec.get("category") or "") == "llm"
|
|
15354
15471
|
and str(spec.get("id")).lower() in {n.lower() for n in connected}
|
|
15355
15472
|
for spec in PROVIDER_CONNECT_CATALOG
|
|
@@ -15360,6 +15477,48 @@ def hermes_models(profile: str = "", user_id: str = "", workspace_id: str = "")
|
|
|
15360
15477
|
if str(spec.get("category") or "") == "llm"
|
|
15361
15478
|
and str(spec.get("id")).lower() in {n.lower() for n in connected}
|
|
15362
15479
|
}
|
|
15480
|
+
|
|
15481
|
+
if selection_only:
|
|
15482
|
+
user_primary, user_chain = _read_user_llm_prefs(user_id) if user_id else ("", [])
|
|
15483
|
+
primary = user_primary or (HERMES_DEFAULT_PRIMARY if has_llm else "")
|
|
15484
|
+
billing = _resolve_billing_provider_for_model(primary, connected_llm) if primary else ""
|
|
15485
|
+
suggestions = _augment_model_suggestions(
|
|
15486
|
+
_suggestions_for_connected_llm_providers(connected_llm) if connected_llm else [],
|
|
15487
|
+
{
|
|
15488
|
+
"default": primary,
|
|
15489
|
+
"provider": billing or "",
|
|
15490
|
+
"base_url": "",
|
|
15491
|
+
"providers": {},
|
|
15492
|
+
"fallback_chain": user_chain,
|
|
15493
|
+
},
|
|
15494
|
+
)
|
|
15495
|
+
return {
|
|
15496
|
+
"ok": True,
|
|
15497
|
+
"profile": "",
|
|
15498
|
+
"primary": primary if has_llm else "",
|
|
15499
|
+
"provider": billing if has_llm else "",
|
|
15500
|
+
"base_url": "",
|
|
15501
|
+
"fallback_chain": user_chain if has_llm else [],
|
|
15502
|
+
"suggestions": suggestions,
|
|
15503
|
+
"connected_providers": sorted(picker_llm if user_id else connected),
|
|
15504
|
+
"has_llm_provider": has_llm,
|
|
15505
|
+
"billing_provider": billing if has_llm else "",
|
|
15506
|
+
"selection_only": True,
|
|
15507
|
+
"default_primary": HERMES_DEFAULT_PRIMARY,
|
|
15508
|
+
"default_fallback_chain": HERMES_DEFAULT_FALLBACK_CHAIN,
|
|
15509
|
+
}
|
|
15510
|
+
|
|
15511
|
+
try:
|
|
15512
|
+
primary_profile = _resolve_models_profile(profile) if profile else _primary_profile()
|
|
15513
|
+
except ValueError as exc:
|
|
15514
|
+
return {"ok": False, "error": str(exc)}
|
|
15515
|
+
if primary_profile:
|
|
15516
|
+
_bootstrap_profile_providers(primary_profile, user_id, workspace_id)
|
|
15517
|
+
block = _read_model_block(primary_profile) if primary_profile else {
|
|
15518
|
+
"default": "", "provider": "", "base_url": "",
|
|
15519
|
+
"providers": {}, "fallback_chain": [],
|
|
15520
|
+
}
|
|
15521
|
+
active_provider = block.get("provider", "").strip()
|
|
15363
15522
|
suggestions = _augment_model_suggestions(
|
|
15364
15523
|
_suggestions_for_connected_llm_providers(connected_llm) if connected_llm else [],
|
|
15365
15524
|
block,
|
|
@@ -15384,16 +15543,25 @@ def hermes_models(profile: str = "", user_id: str = "", workspace_id: str = "")
|
|
|
15384
15543
|
seen_models.add(mid)
|
|
15385
15544
|
deduped.append(row)
|
|
15386
15545
|
suggestions = deduped
|
|
15546
|
+
primary_model = str(block.get("default") or "").strip()
|
|
15547
|
+
billing = _billing_provider_id_from_hermes_config(active_provider)
|
|
15548
|
+
if not billing and primary_model:
|
|
15549
|
+
billing = _resolve_billing_provider_for_model(primary_model, connected_llm) or ""
|
|
15550
|
+
if str(active_provider or "").lower() == "custom" and billing:
|
|
15551
|
+
display_provider = billing
|
|
15552
|
+
else:
|
|
15553
|
+
display_provider = active_provider
|
|
15387
15554
|
return {
|
|
15388
15555
|
"ok": True,
|
|
15389
15556
|
"profile": primary_profile,
|
|
15390
|
-
"primary":
|
|
15391
|
-
"provider":
|
|
15557
|
+
"primary": primary_model if has_llm else "",
|
|
15558
|
+
"provider": display_provider if has_llm else "",
|
|
15392
15559
|
"base_url": block.get("base_url", ""),
|
|
15393
15560
|
"fallback_chain": block.get("fallback_chain", []) if has_llm else [],
|
|
15394
15561
|
"suggestions": suggestions,
|
|
15395
15562
|
"connected_providers": sorted(picker_llm if user_id else connected),
|
|
15396
15563
|
"has_llm_provider": has_llm,
|
|
15564
|
+
"billing_provider": billing if has_llm else "",
|
|
15397
15565
|
"default_primary": HERMES_DEFAULT_PRIMARY,
|
|
15398
15566
|
"default_fallback_chain": HERMES_DEFAULT_FALLBACK_CHAIN,
|
|
15399
15567
|
}
|
|
@@ -15428,8 +15596,27 @@ def hermes_model_set(
|
|
|
15428
15596
|
model_id: str,
|
|
15429
15597
|
user_id: str = "",
|
|
15430
15598
|
workspace_id: str = "",
|
|
15599
|
+
*,
|
|
15600
|
+
selection_only: bool = False,
|
|
15431
15601
|
) -> dict[str, Any]:
|
|
15432
15602
|
"""Set primary model and align Hermes provider routing to a connected billing provider."""
|
|
15603
|
+
user = str(user_id or "").strip()
|
|
15604
|
+
if selection_only:
|
|
15605
|
+
if not user:
|
|
15606
|
+
return {"ok": False, "error": "unauthorized"}
|
|
15607
|
+
connected = _user_llm_providers_for_picker(user)
|
|
15608
|
+
billing = _resolve_billing_provider_for_model(model_id, connected)
|
|
15609
|
+
if not billing and not _user_llm_has_provider(user, workspace_id):
|
|
15610
|
+
return {"ok": False, "error": "connect an LLM provider first"}
|
|
15611
|
+
_write_user_llm_prefs(user, primary=model_id)
|
|
15612
|
+
return {
|
|
15613
|
+
"ok": True,
|
|
15614
|
+
"profile": "",
|
|
15615
|
+
"model": model_id,
|
|
15616
|
+
"provider": billing or "",
|
|
15617
|
+
"billing_provider": billing or "",
|
|
15618
|
+
"selection_only": True,
|
|
15619
|
+
}
|
|
15433
15620
|
target = profile or _primary_profile()
|
|
15434
15621
|
if not target:
|
|
15435
15622
|
return {"ok": False, "error": "no profile resolved"}
|
|
@@ -15446,6 +15633,11 @@ def hermes_model_set(
|
|
|
15446
15633
|
ok, err = _set_profile_model(target, model_id)
|
|
15447
15634
|
if not ok:
|
|
15448
15635
|
return {"ok": False, "error": err}
|
|
15636
|
+
tpl = safe_profile_slug(target)
|
|
15637
|
+
if user and not _is_runtime_profile_slug(tpl):
|
|
15638
|
+
runtime = _runtime_profile_slug(user, tpl)
|
|
15639
|
+
if _runtime_profile_on_disk(runtime):
|
|
15640
|
+
_sync_runtime_model_from_template(runtime, tpl)
|
|
15449
15641
|
block = _read_model_block(target)
|
|
15450
15642
|
return {
|
|
15451
15643
|
"ok": True,
|
|
@@ -15458,14 +15650,17 @@ def hermes_model_set(
|
|
|
15458
15650
|
}
|
|
15459
15651
|
|
|
15460
15652
|
|
|
15461
|
-
def hermes_fallback_chain_set(
|
|
15653
|
+
def hermes_fallback_chain_set(
|
|
15654
|
+
profile: str,
|
|
15655
|
+
chain: list[Any],
|
|
15656
|
+
*,
|
|
15657
|
+
selection_only: bool = False,
|
|
15658
|
+
user_id: str = "",
|
|
15659
|
+
) -> dict[str, Any]:
|
|
15462
15660
|
"""Set the fallback chain for a profile. Each entry must have
|
|
15463
15661
|
`provider` and `model` keys. Order matters: Hermes tries them in
|
|
15464
15662
|
sequence when the primary fails.
|
|
15465
15663
|
"""
|
|
15466
|
-
target = profile or _primary_profile()
|
|
15467
|
-
if not target:
|
|
15468
|
-
return {"ok": False, "error": "no profile resolved"}
|
|
15469
15664
|
normalized: list[dict[str, str]] = []
|
|
15470
15665
|
for entry in chain:
|
|
15471
15666
|
if not isinstance(entry, dict):
|
|
@@ -15478,9 +15673,24 @@ def hermes_fallback_chain_set(profile: str, chain: list[Any]) -> dict[str, Any]:
|
|
|
15478
15673
|
"error": f"invalid fallback entry: provider={provider!r} model={model!r}",
|
|
15479
15674
|
}
|
|
15480
15675
|
normalized.append({"provider": provider, "model": model})
|
|
15676
|
+
if selection_only:
|
|
15677
|
+
user = str(user_id or "").strip()
|
|
15678
|
+
if not user:
|
|
15679
|
+
return {"ok": False, "error": "unauthorized"}
|
|
15680
|
+
_write_user_llm_prefs(user, fallback_chain=normalized)
|
|
15681
|
+
return {"ok": True, "profile": "", "fallback_chain": normalized, "selection_only": True}
|
|
15682
|
+
target = profile or _primary_profile()
|
|
15683
|
+
if not target:
|
|
15684
|
+
return {"ok": False, "error": "no profile resolved"}
|
|
15481
15685
|
ok, err = _write_fallback_chain(target, normalized)
|
|
15482
15686
|
if not ok:
|
|
15483
15687
|
return {"ok": False, "error": err}
|
|
15688
|
+
user = str(user_id or "").strip()
|
|
15689
|
+
tpl = safe_profile_slug(target)
|
|
15690
|
+
if user and not _is_runtime_profile_slug(tpl):
|
|
15691
|
+
runtime = _runtime_profile_slug(user, tpl)
|
|
15692
|
+
if _runtime_profile_on_disk(runtime):
|
|
15693
|
+
_sync_runtime_model_from_template(runtime, tpl)
|
|
15484
15694
|
return {"ok": True, "profile": target, "fallback_chain": normalized}
|
|
15485
15695
|
|
|
15486
15696
|
|
|
@@ -16958,7 +17168,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
16958
17168
|
profile = qs.get("profile", [""])[0]
|
|
16959
17169
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
16960
17170
|
workspace_id = qs.get("workspace_id", [""])[0]
|
|
16961
|
-
|
|
17171
|
+
selection_only = qs.get("selection_only", ["0"])[0] in {"1", "true", "yes"}
|
|
17172
|
+
payload = hermes_models(profile, user_id, workspace_id, selection_only=selection_only)
|
|
17173
|
+
status = 200 if payload.get("ok", True) else 400
|
|
17174
|
+
return self._json(status, payload)
|
|
16962
17175
|
if path == "/api/hermes/profiles/status":
|
|
16963
17176
|
profile = resolve_validated_profile(qs.get("profile", [""])[0] or _primary_profile())
|
|
16964
17177
|
if SUPERVISOR_URL:
|
|
@@ -18961,11 +19174,24 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
18961
19174
|
profile = str(body.get("profile", "")).strip()
|
|
18962
19175
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
18963
19176
|
workspace_id = str(body.get("workspace_id", "")).strip()
|
|
19177
|
+
selection_only = bool(body.get("selection_only"))
|
|
18964
19178
|
if not model_id:
|
|
18965
19179
|
return self._json(400, {"error": "model required"})
|
|
18966
|
-
if profile:
|
|
18967
|
-
|
|
18968
|
-
|
|
19180
|
+
if profile and not selection_only:
|
|
19181
|
+
try:
|
|
19182
|
+
_bootstrap_profile_providers(_resolve_models_profile(profile), user_id, workspace_id)
|
|
19183
|
+
except ValueError as exc:
|
|
19184
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
19185
|
+
return self._json(
|
|
19186
|
+
200,
|
|
19187
|
+
hermes_model_set(
|
|
19188
|
+
profile,
|
|
19189
|
+
model_id,
|
|
19190
|
+
user_id,
|
|
19191
|
+
workspace_id,
|
|
19192
|
+
selection_only=selection_only,
|
|
19193
|
+
),
|
|
19194
|
+
)
|
|
18969
19195
|
if path == "/api/hermes/model/apply-default":
|
|
18970
19196
|
if not _check_auth(self):
|
|
18971
19197
|
return self._json(401, {"error": "unauthorized"})
|
|
@@ -18975,13 +19201,23 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
18975
19201
|
if path == "/api/hermes/fallback-chain":
|
|
18976
19202
|
if not _check_auth(self):
|
|
18977
19203
|
return self._json(401, {"error": "unauthorized"})
|
|
18978
|
-
if not _role_allows(self, OWNER_ADMIN_ROLES):
|
|
18979
|
-
return self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
|
|
18980
19204
|
profile = str(body.get("profile", "")).strip()
|
|
18981
19205
|
chain = body.get("chain", [])
|
|
19206
|
+
selection_only = bool(body.get("selection_only"))
|
|
19207
|
+
user_id = str(getattr(self, "auth_user", "") or "")
|
|
18982
19208
|
if not isinstance(chain, list):
|
|
18983
19209
|
return self._json(400, {"error": "chain must be a list"})
|
|
18984
|
-
|
|
19210
|
+
if not selection_only and not _role_allows(self, OWNER_ADMIN_ROLES):
|
|
19211
|
+
return self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
|
|
19212
|
+
return self._json(
|
|
19213
|
+
200,
|
|
19214
|
+
hermes_fallback_chain_set(
|
|
19215
|
+
profile,
|
|
19216
|
+
chain,
|
|
19217
|
+
selection_only=selection_only,
|
|
19218
|
+
user_id=user_id,
|
|
19219
|
+
),
|
|
19220
|
+
)
|
|
18985
19221
|
if path == "/api/board":
|
|
18986
19222
|
if not _check_auth(self):
|
|
18987
19223
|
return self._json(401, {"error": "unauthorized"})
|
|
@@ -251,18 +251,18 @@ def patch_stack_config(body: dict[str, Any]) -> dict[str, Any]:
|
|
|
251
251
|
|
|
252
252
|
|
|
253
253
|
def resolve_deployment_mode(env_default: str = "trusted_team") -> str:
|
|
254
|
-
"""
|
|
255
|
-
env_raw = (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "").strip().lower()
|
|
256
|
-
if env_raw in DEPLOYMENT_MODES:
|
|
257
|
-
return env_raw
|
|
258
|
-
if env_raw:
|
|
259
|
-
return env_raw
|
|
254
|
+
"""Persisted stack_config deployment_mode wins when set; else WORKFRAME_DEPLOYMENT_MODE env."""
|
|
260
255
|
try:
|
|
261
|
-
sc_mode = str(
|
|
256
|
+
sc_mode = str(_read_raw().get("deployment_mode") or "").strip().lower()
|
|
262
257
|
if sc_mode in DEPLOYMENT_MODES:
|
|
263
258
|
return sc_mode
|
|
264
259
|
except Exception:
|
|
265
260
|
pass
|
|
261
|
+
env_raw = (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "").strip().lower()
|
|
262
|
+
if env_raw in DEPLOYMENT_MODES:
|
|
263
|
+
return env_raw
|
|
264
|
+
if env_raw:
|
|
265
|
+
return env_raw
|
|
266
266
|
raw = (env_default or "trusted_team").strip().lower()
|
|
267
267
|
return raw if raw in DEPLOYMENT_MODES else "trusted_team"
|
|
268
268
|
|
|
@@ -48,4 +48,9 @@ resolved = stack_config.resolved_smtp()
|
|
|
48
48
|
assert resolved.get("host") == "smtp.ghost.example", resolved
|
|
49
49
|
assert resolved.get("source") == "env", resolved
|
|
50
50
|
|
|
51
|
+
# Wizard deployment_mode persists over compose .env default
|
|
52
|
+
os.environ["WORKFRAME_DEPLOYMENT_MODE"] = "trusted_team"
|
|
53
|
+
stack_config.patch_stack_config({"deployment_mode": "single_user_local"})
|
|
54
|
+
assert stack_config.resolve_deployment_mode() == "single_user_local"
|
|
55
|
+
|
|
51
56
|
print("stack_config install-window smtp self-check ok")
|