create-workframe 0.1.2 → 0.1.4
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 +14 -1
- package/SECURITY.md +3 -3
- package/bin/create-workframe.js +3 -3
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +13 -7
- package/workframe-api/README.md +3 -5
- package/workframe-api/concierge.py +173 -0
- package/workframe-api/llm_error_glossary.py +236 -0
- package/workframe-api/llm_proxy.py +3 -0
- package/workframe-api/openrouter_catalog.py +114 -0
- package/workframe-api/package.json +1 -1
- package/workframe-api/server.py +178 -50
- package/workframe-api/updates.py +107 -18
- package/workframe-supervisor/server.py +75 -2
- package/workframe-ui/public/assets/{arc-COAT3laO.js → arc-BskqxLmE.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DOxsUI33.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BFjWV24l.js → architectureDiagram-3BPJPVTR-B3eGdqvq.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DSQLPfrj.js → blockDiagram-GPEHLZMM-CzFZG7uX.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DKEHv1t2.js → c4Diagram-AAUBKEIU-BS-gGyCH.js} +1 -1
- package/workframe-ui/public/assets/channel-Baa20pIs.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-DHZg-DUi.js → chunk-2J33WTMH-Cjvqf7c9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-BB-OYTfp.js → chunk-3OPIFGDE--eUuL8AK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-C93q0YIm.js → chunk-4BX2VUAB-s9lHUFKL.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-MAYniqik.js → chunk-55IACEB6-D1McA5nz.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-ChgN6YJs.js → chunk-5ZQYHXKU-DIstLfU2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-B_FYwdAv.js → chunk-727SXJPM-Cd0GD2CO.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-1_Hw_h1A.js → chunk-AQP2D5EJ-gc5Ra6G4.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CFiDQ1Rv.js → chunk-BSJP7CBP-Cg5QfihJ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-DZ9UMTlB.js → chunk-CSCIHK7Q-D_gobbhY.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DlMlyFgw.js → chunk-FMBD7UC4-B6eLY9_d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-DHXtQ_Hf.js → chunk-KSCS5N6A-BGroAeW2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CuQzg-QG.js → chunk-L5ZTLDWV-34xv7WgT.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-BHzjzCGg.js → chunk-LZXEDZCA-XJBctFZw.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-DHXx05n2.js → chunk-ND2GUHAM-nTOZpln8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CV5pmDM_.js → chunk-NZK2D7GU-BBrzcaPH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-6tkCHxsV.js → chunk-O5CBEL6O-Cntncx6L.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-B7tPxs4H.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-Dvh2By1o.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-BClwIiCp.js → chunk-XPW4576I-DSbtd0cg.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BljHyDzF.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BljHyDzF.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DOrGV6DQ.js → cose-bilkent-S5V4N54A-lmYstEMQ.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DXTPvJkX.js → dagre-BM42HDAG-Bdkbdul2.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-xX_v-pbf.js → diagram-2AECGRRQ-Clb9kSXe.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-Cd2pXbBe.js → diagram-5GNKFQAL-BApg0yec.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df3XvUtk.js → diagram-KO2AKTUF-peZjDRV6.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CsijIPaD.js → diagram-LMA3HP47-DZm_1pjc.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-aq5fmfHd.js → diagram-OG6HWLK6-DP_N6fYL.js} +1 -1
- package/workframe-ui/public/assets/{dist-D1c0mkbB.js → dist-CqP43hXK.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-DnFysVRY.js → erDiagram-TEJ5UH35-Blfs6kLv.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-C4xwO6dx.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C6Ebi3su.js → flowDiagram-I6XJVG4X-CcmMOkwH.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-BQXQtUpa.js → ganttDiagram-6RSMTGT7-ByoO3dF7.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-Dt0zIs_M.js → gitGraph-WXDBUCRP-DCyTpmPs.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BF8gHzRn.js → gitGraphDiagram-PVQCEYII-CvLYB55b.js} +1 -1
- package/workframe-ui/public/assets/index-BIfRGM8p.js +133 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CSmszQJT.js → info-J43DQDTF-CfSRIfOI.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CVTKGW6p.js → infoDiagram-5YYISTIA-DwS44DS6.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js → ishikawaDiagram-YF4QCWOH-MRlyodyU.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-r3wD68_T.js → journeyDiagram-JHISSGLW-sq7awk87.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Il8VglqN.js → kanban-definition-UN3LZRKU-CH6HbcWu.js} +1 -1
- package/workframe-ui/public/assets/{line-oyjpfz2A.js → line-DRBjFmUW.js} +1 -1
- package/workframe-ui/public/assets/{linear-Cf7p5tVp.js → linear-BR0LunjE.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-YmbZ-AfY.js → mermaid-parser.core-Cl1GjHkT.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-BFdCAqCo.js → mermaid.core-DsiKK1kE.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Cy2iCtEl.js → mindmap-definition-RKZ34NQL-DBvi3OpW.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-DwOBZL6K.js → packet-YPE3B663-CPDgtUHg.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-04PPhnKK.js → pie-LRSECV5Y-BXJIPPDm.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-LxIpgHqi.js → pieDiagram-4H26LBE5-CZwLEK-o.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-0nBYfYm4.js → quadrantDiagram-W4KKPZXB-B65bEUIQ.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-D2-vBqps.js → radar-GUYGQ44K-vXHkZzRK.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DbuU0nlu.js → requirementDiagram-4Y6WPE33-BGWdvDfG.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-B2hQ6B2x.js → sankeyDiagram-5OEKKPKP-BT2oC8dV.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BBrU30e1.js → sequenceDiagram-3UESZ5HK-45yj0Wtx.js} +1 -1
- package/workframe-ui/public/assets/{src-BJEDmV70.js → src-D-P8U2PB.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-7FGO4kkH.js → stateDiagram-AJRCARHV-SBrBJBZG.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B3im3So2.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-ptDm4rCN.js → timeline-definition-PNZ67QCA-DegmxXXj.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CS6Z-0q8.js → treeView-BLDUP644-BVy7CsCj.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DqV4Y2VA.js → treemap-LRROVOQU-BD5Kb74J.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-C0UrZJYt.js → vennDiagram-CIIHVFJN-hwAkNbl3.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-bNDN3_Sa.js → wardley-L42UT6IY-CBMPT8Y1.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-jWiJsefM.js → wardleyDiagram-YWT4CUSO-DXqihVlE.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-Dsh_fLCy.js → xychartDiagram-2RQKCTM6-Cbb4LBzj.js} +1 -1
- package/workframe-ui/public/index.html +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +0 -1
- package/workframe-ui/public/assets/channel-g7r_RGaY.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +0 -1
- package/workframe-ui/public/assets/index-lRpzpNPT.js +0 -133
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +0 -1
package/workframe-api/server.py
CHANGED
|
@@ -46,6 +46,9 @@ import turn_credentials
|
|
|
46
46
|
import llm_proxy
|
|
47
47
|
import action_proxy
|
|
48
48
|
import internal_proxy_auth
|
|
49
|
+
import concierge
|
|
50
|
+
import llm_error_glossary
|
|
51
|
+
import openrouter_catalog
|
|
49
52
|
import updates as stack_updates
|
|
50
53
|
|
|
51
54
|
HERMES_DATA = Path(os.environ.get("HERMES_DATA", "/opt/data"))
|
|
@@ -548,12 +551,26 @@ def _admin_write_allowed(handler: BaseHTTPRequestHandler) -> bool:
|
|
|
548
551
|
|
|
549
552
|
|
|
550
553
|
def _apply_stack_update(target: str) -> dict[str, Any]:
|
|
554
|
+
workframe_version = ""
|
|
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 = ""
|
|
551
562
|
try:
|
|
552
563
|
return stack_updates.apply_update(target)
|
|
553
564
|
except ValueError as exc:
|
|
554
|
-
|
|
565
|
+
err = str(exc)
|
|
566
|
+
if not _supervisor_ready():
|
|
567
|
+
raise
|
|
568
|
+
if err not in ("docker_unavailable", "admin_updates_disabled"):
|
|
555
569
|
raise
|
|
556
|
-
|
|
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)
|
|
557
574
|
if status >= 300 or not isinstance(data, dict) or not data.get("ok"):
|
|
558
575
|
raise ValueError(str((data or {}).get("error") or "supervisor_apply_failed"))
|
|
559
576
|
return data
|
|
@@ -2638,6 +2655,24 @@ def _spawn_hermes_device_oauth(user_id: str, hermes_auth_id: str, log_container:
|
|
|
2638
2655
|
"""Start long-running `hermes auth add` detached in gateway (survives exec return)."""
|
|
2639
2656
|
home = _hermes_user_home_container(user_id)
|
|
2640
2657
|
_user_hermes_home(user_id).mkdir(parents=True, exist_ok=True)
|
|
2658
|
+
if SECURE_MODE and _supervisor_ready():
|
|
2659
|
+
status, data = _supervisor_request(
|
|
2660
|
+
"POST",
|
|
2661
|
+
"/v1/hermes.device_oauth_start",
|
|
2662
|
+
{"home": home, "hermes_auth_id": hermes_auth_id, "log_path": log_container},
|
|
2663
|
+
timeout=30.0,
|
|
2664
|
+
)
|
|
2665
|
+
if status >= 300:
|
|
2666
|
+
err = data.get("error") if isinstance(data, dict) else str(data)
|
|
2667
|
+
raise ValueError(err or "device_oauth_start_failed")
|
|
2668
|
+
if not isinstance(data, dict):
|
|
2669
|
+
raise ValueError("device_oauth_start_invalid")
|
|
2670
|
+
exit_code = data.get("exit_code")
|
|
2671
|
+
try:
|
|
2672
|
+
code = int(exit_code if exit_code is not None else 1)
|
|
2673
|
+
except (TypeError, ValueError):
|
|
2674
|
+
code = 1
|
|
2675
|
+
return code, str(data.get("output") or "")
|
|
2641
2676
|
auth_cmd = " ".join(shlex.quote(part) for part in ["auth", "add", hermes_auth_id])
|
|
2642
2677
|
shell = (
|
|
2643
2678
|
f"mkdir -p {shlex.quote(home)}; "
|
|
@@ -12302,8 +12337,9 @@ def _provider_pool_entry(provider: str, env_var: str, *, base_url: str = "") ->
|
|
|
12302
12337
|
|
|
12303
12338
|
|
|
12304
12339
|
def _runtime_provider_pool_entry(provider: str, env_var: str) -> dict[str, Any]:
|
|
12305
|
-
"""Credential pool row for u-* profiles —
|
|
12306
|
-
|
|
12340
|
+
"""Credential pool row for u-* profiles — lease token lives in OPENAI_API_KEY."""
|
|
12341
|
+
lease_env = "OPENAI_API_KEY"
|
|
12342
|
+
return _provider_pool_entry(provider, lease_env, base_url=_llm_proxy_base_url(provider))
|
|
12307
12343
|
|
|
12308
12344
|
|
|
12309
12345
|
def _provider_pool_has_entries(auth: dict[str, Any], provider: str) -> bool:
|
|
@@ -13313,23 +13349,75 @@ def _enrich_room_chat_payload(payload: dict[str, Any], user_id: str) -> dict[str
|
|
|
13313
13349
|
return body
|
|
13314
13350
|
|
|
13315
13351
|
|
|
13316
|
-
def _emit_stream_chat_error(
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13352
|
+
def _emit_stream_chat_error(
|
|
13353
|
+
handler: BaseHTTPRequestHandler,
|
|
13354
|
+
message: str = "",
|
|
13355
|
+
*,
|
|
13356
|
+
entry: dict[str, Any] | None = None,
|
|
13357
|
+
) -> None:
|
|
13358
|
+
"""SSE error frame — structured playbook when entry supplied."""
|
|
13359
|
+
payload = llm_error_glossary.notice_payload(entry) if entry else {}
|
|
13360
|
+
if not payload:
|
|
13361
|
+
text = str(message or "").strip() or "Chat failed."
|
|
13362
|
+
payload = {"error": text, "text": text, "message": text}
|
|
13320
13363
|
try:
|
|
13321
13364
|
handler.send_response(200)
|
|
13322
13365
|
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
13323
13366
|
handler.send_header("Cache-Control", "no-cache")
|
|
13324
13367
|
handler.send_header("Connection", "keep-alive")
|
|
13325
13368
|
handler.end_headers()
|
|
13326
|
-
|
|
13369
|
+
body = json.dumps(payload)
|
|
13370
|
+
handler.wfile.write(f"event: error\ndata: {body}\n\n".encode("utf-8"))
|
|
13327
13371
|
handler.wfile.write(b"event: done\ndata: {}\n\n")
|
|
13328
13372
|
handler.wfile.flush()
|
|
13329
13373
|
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
13330
13374
|
return
|
|
13331
13375
|
|
|
13332
13376
|
|
|
13377
|
+
def _emit_stream_concierge(handler: BaseHTTPRequestHandler, entry: dict[str, Any]) -> None:
|
|
13378
|
+
"""Deterministic assistant reply when LLM path is unavailable."""
|
|
13379
|
+
payload = llm_error_glossary.notice_payload(entry)
|
|
13380
|
+
text = str(payload.get("message") or "")
|
|
13381
|
+
hint = str(payload.get("hint") or "").strip()
|
|
13382
|
+
if hint:
|
|
13383
|
+
text = f"{text}\n\n{hint}"
|
|
13384
|
+
try:
|
|
13385
|
+
handler.send_response(200)
|
|
13386
|
+
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
13387
|
+
handler.send_header("Cache-Control", "no-cache")
|
|
13388
|
+
handler.send_header("Connection", "keep-alive")
|
|
13389
|
+
handler.end_headers()
|
|
13390
|
+
handler.wfile.write(
|
|
13391
|
+
f"event: concierge\ndata: {json.dumps(payload)}\n\n".encode("utf-8"),
|
|
13392
|
+
)
|
|
13393
|
+
handler.wfile.write(
|
|
13394
|
+
f"event: message.complete\ndata: {json.dumps({'content': text, 'text': text})}\n\n".encode(
|
|
13395
|
+
"utf-8",
|
|
13396
|
+
),
|
|
13397
|
+
)
|
|
13398
|
+
handler.wfile.write(b"event: done\ndata: {}\n\n")
|
|
13399
|
+
handler.wfile.flush()
|
|
13400
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
13401
|
+
return
|
|
13402
|
+
|
|
13403
|
+
|
|
13404
|
+
def _log_llm_failure(
|
|
13405
|
+
handler: BaseHTTPRequestHandler,
|
|
13406
|
+
code: str,
|
|
13407
|
+
*,
|
|
13408
|
+
provider: str = "",
|
|
13409
|
+
model: str = "",
|
|
13410
|
+
profile: str = "",
|
|
13411
|
+
) -> None:
|
|
13412
|
+
if hasattr(handler, "_log_audit"):
|
|
13413
|
+
handler._log_audit( # type: ignore[attr-defined]
|
|
13414
|
+
"llm_failure",
|
|
13415
|
+
"llm",
|
|
13416
|
+
profile or provider or "unknown",
|
|
13417
|
+
f"code={code} provider={provider} model={model}",
|
|
13418
|
+
)
|
|
13419
|
+
|
|
13420
|
+
|
|
13333
13421
|
def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload: dict[str, Any]) -> None:
|
|
13334
13422
|
_triggering_user = str(payload.get("user_id", "") or "")
|
|
13335
13423
|
_workspace_id = str(payload.get("workspace_id", "") or "")
|
|
@@ -13345,6 +13433,15 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
13345
13433
|
if not text:
|
|
13346
13434
|
raise ValueError("text required")
|
|
13347
13435
|
llm_provider = _llm_billing_provider(prof)
|
|
13436
|
+
llm_ready = bool(
|
|
13437
|
+
_triggering_user
|
|
13438
|
+
and _user_can_use_llm(_triggering_user, _workspace_id, llm_provider)
|
|
13439
|
+
)
|
|
13440
|
+
if _triggering_user and not llm_ready:
|
|
13441
|
+
entry = concierge.respond(text, situation="no_provider")
|
|
13442
|
+
_log_llm_failure(handler, str(entry.get("code") or "no_llm_provider"), provider=llm_provider, profile=prof)
|
|
13443
|
+
_emit_stream_concierge(handler, entry)
|
|
13444
|
+
return
|
|
13348
13445
|
try:
|
|
13349
13446
|
lifecycle = ensure_profile_api(
|
|
13350
13447
|
prof,
|
|
@@ -13358,7 +13455,15 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
13358
13455
|
)
|
|
13359
13456
|
_overlay_turn_user_env(prof, _triggering_user, _workspace_id, _turn_run_id)
|
|
13360
13457
|
except ValueError as exc:
|
|
13361
|
-
|
|
13458
|
+
err_text = str(exc)
|
|
13459
|
+
if "no_llm_provider_for_user" in err_text:
|
|
13460
|
+
entry = concierge.respond(text, situation="no_provider")
|
|
13461
|
+
_log_llm_failure(handler, "no_llm_provider", provider=llm_provider, profile=prof)
|
|
13462
|
+
_emit_stream_concierge(handler, entry)
|
|
13463
|
+
return
|
|
13464
|
+
entry = llm_error_glossary.classify_exception_text(err_text)
|
|
13465
|
+
_log_llm_failure(handler, str(entry.get("code") or ""), provider=llm_provider, profile=prof)
|
|
13466
|
+
_emit_stream_chat_error(handler, entry=entry)
|
|
13362
13467
|
return
|
|
13363
13468
|
upstream_body = json.dumps(_profile_turn_payload(prof, text, _room_id)).encode("utf-8")
|
|
13364
13469
|
|
|
@@ -13487,34 +13592,21 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
13487
13592
|
handler.wfile.flush()
|
|
13488
13593
|
if not saw_complete or not complete_text:
|
|
13489
13594
|
config_key = _read_config_model_api_key(prof)
|
|
13595
|
+
_, model_name = _read_model_from_config(prof)
|
|
13490
13596
|
if config_key.startswith(turn_credentials.LEASE_PREFIX) and not turn_credentials.validate_lease(
|
|
13491
13597
|
config_key,
|
|
13492
13598
|
):
|
|
13493
|
-
|
|
13494
|
-
"Turn credential expired before the model replied. "
|
|
13495
|
-
"Send the message again."
|
|
13496
|
-
)
|
|
13497
|
-
err_code = "invalid_lease"
|
|
13498
|
-
elif not saw_complete:
|
|
13499
|
-
err_text = (
|
|
13500
|
-
"Model provider returned no reply. "
|
|
13501
|
-
"The request may have failed at the gateway or provider."
|
|
13502
|
-
)
|
|
13503
|
-
err_code = "provider_empty_reply"
|
|
13599
|
+
entry = llm_error_glossary.playbook_entry("invalid_lease")
|
|
13504
13600
|
else:
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
)
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
"error": err_text,
|
|
13513
|
-
"text": err_text,
|
|
13514
|
-
"message": err_text,
|
|
13515
|
-
"code": err_code,
|
|
13516
|
-
},
|
|
13601
|
+
entry = llm_error_glossary.playbook_entry("provider_empty_reply")
|
|
13602
|
+
_log_llm_failure(
|
|
13603
|
+
handler,
|
|
13604
|
+
str(entry.get("code") or "provider_empty_reply"),
|
|
13605
|
+
provider=llm_provider,
|
|
13606
|
+
model=str(model_name or ""),
|
|
13607
|
+
profile=prof,
|
|
13517
13608
|
)
|
|
13609
|
+
err_payload = json.dumps(llm_error_glossary.notice_payload(entry))
|
|
13518
13610
|
handler.wfile.write(f"event: error\ndata: {err_payload}\n\n".encode("utf-8"))
|
|
13519
13611
|
handler.wfile.flush()
|
|
13520
13612
|
handler.wfile.write(b"event: done\ndata: {}\n\n")
|
|
@@ -14252,16 +14344,16 @@ def _resolve_command(token: str) -> dict[str, Any] | None:
|
|
|
14252
14344
|
# overrides live in the profile's config.yaml under `model.default` and
|
|
14253
14345
|
# `fallback_providers`; this constant is the install-wide default the
|
|
14254
14346
|
# migration writes when a profile is missing either field.
|
|
14255
|
-
HERMES_DEFAULT_PRIMARY = "
|
|
14347
|
+
HERMES_DEFAULT_PRIMARY = "google/gemini-2.5-flash"
|
|
14256
14348
|
HERMES_DEFAULT_FALLBACK_CHAIN: list[dict[str, str]] = [
|
|
14257
|
-
{"provider": "openrouter", "model": "
|
|
14258
|
-
{"provider": "openrouter", "model": "
|
|
14349
|
+
{"provider": "openrouter", "model": "anthropic/claude-sonnet-4.5"},
|
|
14350
|
+
{"provider": "openrouter", "model": "meta-llama/llama-3.3-70b-instruct:free"},
|
|
14259
14351
|
]
|
|
14260
14352
|
|
|
14261
14353
|
# ponytail: minimum-viable agentic models per LLM provider; user can escalate in settings.
|
|
14262
14354
|
PROVIDER_MVP_MODELS: dict[str, dict[str, Any]] = {
|
|
14263
14355
|
"openrouter": {
|
|
14264
|
-
"primary": "
|
|
14356
|
+
"primary": "google/gemini-2.5-flash",
|
|
14265
14357
|
"fallbacks": list(HERMES_DEFAULT_FALLBACK_CHAIN),
|
|
14266
14358
|
},
|
|
14267
14359
|
"openai": {
|
|
@@ -14273,8 +14365,8 @@ PROVIDER_MVP_MODELS: dict[str, dict[str, Any]] = {
|
|
|
14273
14365
|
"fallbacks": [{"provider": "anthropic", "model": "claude-3-5-haiku-20241022"}],
|
|
14274
14366
|
},
|
|
14275
14367
|
"google": {
|
|
14276
|
-
"primary": "gemini-2.
|
|
14277
|
-
"fallbacks": [{"provider": "google", "model": "gemini-2.
|
|
14368
|
+
"primary": "gemini-2.5-flash",
|
|
14369
|
+
"fallbacks": [{"provider": "google", "model": "gemini-2.5-flash-lite"}],
|
|
14278
14370
|
},
|
|
14279
14371
|
"deepseek": {
|
|
14280
14372
|
"primary": "deepseek-chat",
|
|
@@ -14285,8 +14377,8 @@ PROVIDER_MVP_MODELS: dict[str, dict[str, Any]] = {
|
|
|
14285
14377
|
"fallbacks": [{"provider": "openai-codex", "model": "gpt-5.4-medium"}],
|
|
14286
14378
|
},
|
|
14287
14379
|
"nous": {
|
|
14288
|
-
"primary": "
|
|
14289
|
-
"fallbacks": [{"provider": "nous", "model": "
|
|
14380
|
+
"primary": "nousresearch/hermes-4-70b",
|
|
14381
|
+
"fallbacks": [{"provider": "nous", "model": "nousresearch/hermes-4-70b"}],
|
|
14290
14382
|
},
|
|
14291
14383
|
}
|
|
14292
14384
|
|
|
@@ -14394,15 +14486,21 @@ HERMES_MODEL_CATALOG: list[dict[str, str]] = [
|
|
|
14394
14486
|
{"provider": "OpenRouter", "model": "openrouter/auto",
|
|
14395
14487
|
"label": "OpenRouter Auto",
|
|
14396
14488
|
"description": "OpenRouter picks the best model per prompt."},
|
|
14397
|
-
{"provider": "OpenRouter", "model": "
|
|
14398
|
-
"label": "
|
|
14399
|
-
"description": "
|
|
14400
|
-
{"provider": "OpenRouter", "model": "
|
|
14401
|
-
"label": "
|
|
14402
|
-
"description": "
|
|
14403
|
-
{"provider": "OpenRouter", "model": "
|
|
14404
|
-
"label": "
|
|
14405
|
-
"description": "
|
|
14489
|
+
{"provider": "OpenRouter", "model": "google/gemini-2.5-flash",
|
|
14490
|
+
"label": "Gemini 2.5 Flash",
|
|
14491
|
+
"description": "Fast, capable default for OpenRouter BYOK."},
|
|
14492
|
+
{"provider": "OpenRouter", "model": "anthropic/claude-sonnet-4.5",
|
|
14493
|
+
"label": "Claude Sonnet 4.5 (via OR)",
|
|
14494
|
+
"description": "Strong agentic fallback on OpenRouter."},
|
|
14495
|
+
{"provider": "OpenRouter", "model": "meta-llama/llama-3.3-70b-instruct:free",
|
|
14496
|
+
"label": "Llama 3.3 70B (free)",
|
|
14497
|
+
"description": "Free-tier OpenRouter fallback when available."},
|
|
14498
|
+
{"provider": "OpenRouter", "model": "z-ai/glm-5.1",
|
|
14499
|
+
"label": "GLM 5.1",
|
|
14500
|
+
"description": "High-availability model on OpenRouter."},
|
|
14501
|
+
{"provider": "OpenRouter", "model": "nousresearch/hermes-4-70b",
|
|
14502
|
+
"label": "Hermes 4 70B",
|
|
14503
|
+
"description": "Agent-tuned model on OpenRouter."},
|
|
14406
14504
|
{"provider": "OpenRouter", "model": "openrouter/nvidia/llama-3.1-nemotron-70b-instruct",
|
|
14407
14505
|
"label": "Nemotron 70B Instruct",
|
|
14408
14506
|
"description": "NVIDIA Nemotron via OpenRouter."},
|
|
@@ -14780,6 +14878,17 @@ def hermes_models(profile: str = "", user_id: str = "", workspace_id: str = "")
|
|
|
14780
14878
|
] if (picker_llm or connected) else [],
|
|
14781
14879
|
block,
|
|
14782
14880
|
)
|
|
14881
|
+
if user_id and "openrouter" in {n.lower() for n in (picker_llm or connected)}:
|
|
14882
|
+
resolved = _resolve_credential(user_id, workspace_id, "openrouter", user_only=True)
|
|
14883
|
+
secret = _credential_secret(resolved or {}, user_id) if resolved else ""
|
|
14884
|
+
live = openrouter_catalog.live_suggestions(secret, limit=30)
|
|
14885
|
+
if live:
|
|
14886
|
+
seen = {str(r.get("model") or "") for r in suggestions}
|
|
14887
|
+
suggestions = [
|
|
14888
|
+
{**row, "label": f"Live · {row.get('label', row.get('model', ''))}"}
|
|
14889
|
+
for row in live
|
|
14890
|
+
if str(row.get("model") or "") not in seen
|
|
14891
|
+
] + suggestions
|
|
14783
14892
|
return {
|
|
14784
14893
|
"ok": True,
|
|
14785
14894
|
"profile": primary_profile,
|
|
@@ -17391,11 +17500,29 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
17391
17500
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
17392
17501
|
(cred_id, None, user_id, None, provider, credential_type, cred_ref, label, 1, user_id, now, now),
|
|
17393
17502
|
)
|
|
17503
|
+
conn.execute(
|
|
17504
|
+
"""UPDATE credential_bindings
|
|
17505
|
+
SET is_active = 0, deleted_at = ?, updated_at = ?
|
|
17506
|
+
WHERE user_id = ? AND provider = ? AND deleted_at IS NULL AND id != ?""",
|
|
17507
|
+
(now, now, user_id, provider, cred_id),
|
|
17508
|
+
)
|
|
17394
17509
|
conn.commit()
|
|
17395
17510
|
conn.close()
|
|
17396
17511
|
except sqlite3.Error as exc:
|
|
17397
17512
|
return self._json(500, {"ok": False, "error": f"db_error: {exc}"})
|
|
17398
17513
|
self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
|
|
17514
|
+
health: dict[str, Any] = {}
|
|
17515
|
+
if provider == "openrouter":
|
|
17516
|
+
secret_probe = _credential_secret(
|
|
17517
|
+
{
|
|
17518
|
+
"credential_ref": cred_ref,
|
|
17519
|
+
"scope": "user",
|
|
17520
|
+
"user_id": user_id,
|
|
17521
|
+
},
|
|
17522
|
+
user_id,
|
|
17523
|
+
)
|
|
17524
|
+
if secret_probe:
|
|
17525
|
+
health = openrouter_catalog.probe_account(secret_probe)
|
|
17399
17526
|
_bootstrap_model_after_llm_connect(user_id, str(body.get("workspace_id") or ""), provider)
|
|
17400
17527
|
return self._json(200, {
|
|
17401
17528
|
"ok": True,
|
|
@@ -17407,6 +17534,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
17407
17534
|
"user_id": user_id,
|
|
17408
17535
|
"created_at": now,
|
|
17409
17536
|
"updated_at": now,
|
|
17537
|
+
"health": health,
|
|
17410
17538
|
**payload,
|
|
17411
17539
|
})
|
|
17412
17540
|
|
package/workframe-api/updates.py
CHANGED
|
@@ -190,6 +190,38 @@ def _host_compose_ready() -> bool:
|
|
|
190
190
|
return compose.joinpath("docker-compose.yml").is_file()
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def _supervisor_configured() -> bool:
|
|
194
|
+
return bool(os.environ.get("WORKFRAME_SUPERVISOR_URL", "").strip()) and bool(
|
|
195
|
+
os.environ.get("WORKFRAME_SUPERVISOR_TOKEN", "").strip()
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _supervisor_gateway_image_digest() -> tuple[str, str]:
|
|
200
|
+
"""Read gateway image digest via workframe-supervisor (SECURE_MODE API has no docker.sock)."""
|
|
201
|
+
base = str(os.environ.get("WORKFRAME_SUPERVISOR_URL", "")).rstrip("/")
|
|
202
|
+
token = str(os.environ.get("WORKFRAME_SUPERVISOR_TOKEN", "")).strip()
|
|
203
|
+
if not base or not token:
|
|
204
|
+
return "", ""
|
|
205
|
+
req = urllib.request.Request(
|
|
206
|
+
f"{base}/v1/gateway.image",
|
|
207
|
+
headers={"Authorization": f"Bearer {token}", "Accept": "application/json", "User-Agent": "workframe-api"},
|
|
208
|
+
)
|
|
209
|
+
try:
|
|
210
|
+
with urllib.request.urlopen(req, timeout=15.0) as resp:
|
|
211
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
212
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):
|
|
213
|
+
return "", ""
|
|
214
|
+
if not isinstance(data, dict) or not data.get("ok"):
|
|
215
|
+
return "", ""
|
|
216
|
+
return str(data.get("digest") or "").strip(), str(data.get("ref") or "").strip()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _admin_stack_updates_enabled() -> bool:
|
|
220
|
+
if os.environ.get("WORKFRAME_ENABLE_ADMIN_UPDATES") == "1":
|
|
221
|
+
return True
|
|
222
|
+
return _supervisor_configured()
|
|
223
|
+
|
|
224
|
+
|
|
193
225
|
def _docker_apply_ready() -> tuple[bool, str | None]:
|
|
194
226
|
if not Path(DOCKER_SOCK).exists():
|
|
195
227
|
return False, "Docker socket is not available to the API container."
|
|
@@ -202,6 +234,29 @@ def _docker_apply_ready() -> tuple[bool, str | None]:
|
|
|
202
234
|
return True, None
|
|
203
235
|
|
|
204
236
|
|
|
237
|
+
def _update_apply_channel() -> tuple[str, bool, str | None]:
|
|
238
|
+
"""Returns (channel, ready, reason). channel: api_docker | supervisor | none."""
|
|
239
|
+
api_docker = Path(DOCKER_SOCK).exists()
|
|
240
|
+
if api_docker:
|
|
241
|
+
ok, reason = _docker_apply_ready()
|
|
242
|
+
if ok:
|
|
243
|
+
return "api_docker", True, None
|
|
244
|
+
if not _supervisor_configured():
|
|
245
|
+
return "none", False, reason
|
|
246
|
+
if _supervisor_configured():
|
|
247
|
+
if _script_path("apply-update-workframe.sh") is None and _script_path("apply-update-hermes.sh") is None:
|
|
248
|
+
return "supervisor", False, "Stack update scripts are missing from this install."
|
|
249
|
+
return "supervisor", True, None
|
|
250
|
+
if api_docker:
|
|
251
|
+
_, reason = _docker_apply_ready()
|
|
252
|
+
return "none", False, reason
|
|
253
|
+
return (
|
|
254
|
+
"none",
|
|
255
|
+
False,
|
|
256
|
+
"In-place updates need workframe-supervisor or Docker on the stack host.",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
205
260
|
def _product_state(*, update_available: bool, can_update: bool) -> str:
|
|
206
261
|
if update_available and can_update:
|
|
207
262
|
return "available"
|
|
@@ -238,7 +293,9 @@ def _releases_manifest() -> dict[str, Any]:
|
|
|
238
293
|
def updates_available(*, desktop_version: str = "", hermes_agent_version: str = "") -> dict[str, Any]:
|
|
239
294
|
compose_dir = _compose_dir()
|
|
240
295
|
project_root = _project_root()
|
|
241
|
-
|
|
296
|
+
api_docker = Path(DOCKER_SOCK).exists()
|
|
297
|
+
supervisor_ok = _supervisor_configured()
|
|
298
|
+
apply_channel, apply_ready, apply_reason = _update_apply_channel()
|
|
242
299
|
installed = _read_installed_workframe_version(project_root)
|
|
243
300
|
|
|
244
301
|
npm_latest = ""
|
|
@@ -254,15 +311,20 @@ def updates_available(*, desktop_version: str = "", hermes_agent_version: str =
|
|
|
254
311
|
installed_pkg = installed.get("package") or installed.get("api") or ""
|
|
255
312
|
workframe_update = bool(workframe_latest and _version_lt(installed_pkg, workframe_latest))
|
|
256
313
|
|
|
257
|
-
hermes_digest, hermes_ref =
|
|
314
|
+
hermes_digest, hermes_ref = ("", "")
|
|
315
|
+
if api_docker:
|
|
316
|
+
hermes_digest, hermes_ref = _container_image_digest(GATEWAY_CONTAINER)
|
|
317
|
+
elif supervisor_ok:
|
|
318
|
+
hermes_digest, hermes_ref = _supervisor_gateway_image_digest()
|
|
258
319
|
hermes_tag = hermes_ref.rsplit(":", 1)[-1] if hermes_ref and ":" in hermes_ref else HERMES_TAG
|
|
259
320
|
hermes_latest_digest = ""
|
|
260
321
|
try:
|
|
261
322
|
hermes_latest_digest = _docker_hub_digest(HERMES_IMAGE, HERMES_TAG)
|
|
262
323
|
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError):
|
|
263
324
|
pass
|
|
325
|
+
hermes_image_known = bool(api_docker or supervisor_ok)
|
|
264
326
|
hermes_update = bool(
|
|
265
|
-
|
|
327
|
+
hermes_image_known
|
|
266
328
|
and hermes_latest_digest
|
|
267
329
|
and hermes_digest
|
|
268
330
|
and hermes_digest != hermes_latest_digest,
|
|
@@ -275,15 +337,14 @@ def updates_available(*, desktop_version: str = "", hermes_agent_version: str =
|
|
|
275
337
|
if len(digest_short) > 28:
|
|
276
338
|
digest_short = digest_short[:28] + "…"
|
|
277
339
|
|
|
278
|
-
docker_apply_ok, docker_apply_reason = _docker_apply_ready()
|
|
279
340
|
hermes_script_ok = _script_path("apply-update-hermes.sh") is not None
|
|
280
341
|
workframe_script_ok = _script_path("apply-update-workframe.sh") is not None
|
|
281
|
-
hermes_can_update = bool(
|
|
282
|
-
workframe_can_update = bool(
|
|
283
|
-
hermes_reason =
|
|
342
|
+
hermes_can_update = bool(apply_ready and hermes_script_ok)
|
|
343
|
+
workframe_can_update = bool(apply_ready and workframe_script_ok)
|
|
344
|
+
hermes_reason = apply_reason
|
|
284
345
|
if not hermes_reason and hermes_update and not hermes_script_ok:
|
|
285
346
|
hermes_reason = "Hermes update script is missing from this install."
|
|
286
|
-
workframe_reason =
|
|
347
|
+
workframe_reason = apply_reason
|
|
287
348
|
if not workframe_reason and workframe_update and not workframe_script_ok:
|
|
288
349
|
workframe_reason = "Workframe update script is missing from this install."
|
|
289
350
|
if not workframe_reason and workframe_update and not workframe_latest:
|
|
@@ -294,7 +355,11 @@ def updates_available(*, desktop_version: str = "", hermes_agent_version: str =
|
|
|
294
355
|
|
|
295
356
|
return {
|
|
296
357
|
"ok": True,
|
|
297
|
-
"docker_available":
|
|
358
|
+
"docker_available": apply_ready,
|
|
359
|
+
"docker_sock_on_api": api_docker,
|
|
360
|
+
"supervisor_configured": supervisor_ok,
|
|
361
|
+
"update_apply_channel": apply_channel if apply_ready else None,
|
|
362
|
+
"update_apply_ready": apply_ready,
|
|
298
363
|
"compose_dir": str(compose_dir),
|
|
299
364
|
"project_root": str(project_root),
|
|
300
365
|
"workframe": {
|
|
@@ -323,7 +388,7 @@ def updates_available(*, desktop_version: str = "", hermes_agent_version: str =
|
|
|
323
388
|
"reason": hermes_reason,
|
|
324
389
|
"update_mode": "docker-compose-pull",
|
|
325
390
|
"install_kind": "docker",
|
|
326
|
-
"can_restart_gateway": bool(
|
|
391
|
+
"can_restart_gateway": bool(apply_ready and _script_path("restart-gateway-hermes.sh") is not None),
|
|
327
392
|
},
|
|
328
393
|
"desktop": {
|
|
329
394
|
"current": desktop_installed,
|
|
@@ -339,12 +404,33 @@ def updates_available(*, desktop_version: str = "", hermes_agent_version: str =
|
|
|
339
404
|
}
|
|
340
405
|
|
|
341
406
|
|
|
407
|
+
def _apply_env_for_target(target: str) -> dict[str, str]:
|
|
408
|
+
env = os.environ.copy()
|
|
409
|
+
env.setdefault("WORKFRAME_COMPOSE_DIR", str(_compose_dir()))
|
|
410
|
+
env.setdefault("WORKFRAME_PROJECT_ROOT", str(_project_root()))
|
|
411
|
+
if target in {"workframe", "all"}:
|
|
412
|
+
latest = ""
|
|
413
|
+
try:
|
|
414
|
+
latest = str(updates_available().get("workframe", {}).get("latest") or "").strip()
|
|
415
|
+
except Exception: # noqa: BLE001
|
|
416
|
+
latest = ""
|
|
417
|
+
if latest:
|
|
418
|
+
env["WORKFRAME_UPDATE_ALLOW_NPM"] = "1"
|
|
419
|
+
env["WORKFRAME_UPDATE_VERSION"] = latest
|
|
420
|
+
return env
|
|
421
|
+
|
|
422
|
+
|
|
342
423
|
def apply_update(target: str) -> dict[str, Any]:
|
|
343
|
-
if
|
|
424
|
+
if not _admin_stack_updates_enabled():
|
|
344
425
|
raise ValueError("admin_updates_disabled")
|
|
345
426
|
target = str(target or "all").strip().lower()
|
|
346
427
|
if target not in {"hermes", "workframe", "all"}:
|
|
347
428
|
raise ValueError("invalid_update_target")
|
|
429
|
+
_, apply_ready, apply_reason = _update_apply_channel()
|
|
430
|
+
if not apply_ready:
|
|
431
|
+
if not Path(DOCKER_SOCK).exists():
|
|
432
|
+
raise ValueError("docker_unavailable")
|
|
433
|
+
raise ValueError(str(apply_reason or "docker_apply_unavailable"))
|
|
348
434
|
if not Path(DOCKER_SOCK).exists():
|
|
349
435
|
raise ValueError("docker_unavailable")
|
|
350
436
|
|
|
@@ -360,9 +446,7 @@ def apply_update(target: str) -> dict[str, Any]:
|
|
|
360
446
|
raise ValueError("update_script_missing:workframe")
|
|
361
447
|
scripts.append(str(script))
|
|
362
448
|
|
|
363
|
-
env =
|
|
364
|
-
env.setdefault("WORKFRAME_COMPOSE_DIR", str(_compose_dir()))
|
|
365
|
-
env.setdefault("WORKFRAME_PROJECT_ROOT", str(_project_root()))
|
|
449
|
+
env = _apply_env_for_target(target)
|
|
366
450
|
|
|
367
451
|
logs: list[str] = []
|
|
368
452
|
for script in scripts:
|
|
@@ -382,13 +466,15 @@ def apply_update(target: str) -> dict[str, Any]:
|
|
|
382
466
|
|
|
383
467
|
|
|
384
468
|
def restart_gateway() -> dict[str, Any]:
|
|
385
|
-
if
|
|
469
|
+
if not _admin_stack_updates_enabled():
|
|
386
470
|
raise ValueError("admin_updates_disabled")
|
|
471
|
+
_, apply_ready, apply_reason = _update_apply_channel()
|
|
472
|
+
if not apply_ready:
|
|
473
|
+
if not Path(DOCKER_SOCK).exists():
|
|
474
|
+
raise ValueError("docker_unavailable")
|
|
475
|
+
raise ValueError(str(apply_reason or "docker_apply_unavailable"))
|
|
387
476
|
if not Path(DOCKER_SOCK).exists():
|
|
388
477
|
raise ValueError("docker_unavailable")
|
|
389
|
-
docker_apply_ok, docker_apply_reason = _docker_apply_ready()
|
|
390
|
-
if not docker_apply_ok:
|
|
391
|
-
raise ValueError(str(docker_apply_reason or "docker_apply_unavailable"))
|
|
392
478
|
script = _script_path("restart-gateway-hermes.sh")
|
|
393
479
|
if not script:
|
|
394
480
|
raise ValueError("restart_script_missing:gateway")
|
|
@@ -414,4 +500,7 @@ if __name__ == "__main__":
|
|
|
414
500
|
assert _version_lt("0.1.0", "0.1.1")
|
|
415
501
|
assert not _version_lt("0.1.0", "0.1.0")
|
|
416
502
|
assert parse_hermes_version_output("Hermes Agent v0.17.0 (2026.6.19)") == "0.17.0"
|
|
503
|
+
ch, ready, _ = _update_apply_channel()
|
|
504
|
+
assert ch in {"api_docker", "supervisor", "none"}
|
|
505
|
+
assert ready is False or ch != "none"
|
|
417
506
|
print("updates module ok")
|