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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""WF-032 extract: profile_gateway."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import queue
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import sqlite3
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import urllib.request
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from http.server import BaseHTTPRequestHandler
|
|
21
|
+
|
|
22
|
+
import user_prefs
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _srv():
|
|
26
|
+
import server as srv
|
|
27
|
+
|
|
28
|
+
return srv
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _reload_runtime_profile_gateway(profile: str, *, wait_healthy: bool = True) -> None:
|
|
32
|
+
"""Reload Hermes after config.yaml change — gateway restart before stop+start."""
|
|
33
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
34
|
+
if not _srv()._is_runtime_profile_slug(prof) or prof == _srv()._primary_profile():
|
|
35
|
+
return
|
|
36
|
+
with _srv()._gateway_lifecycle_lock:
|
|
37
|
+
if _profile_api_healthy(prof, timeout=0.5, use_cache=False):
|
|
38
|
+
code, _out = _srv()._gateway_exec(prof, ["gateway", "restart"])
|
|
39
|
+
if code == 0:
|
|
40
|
+
_srv()._invalidate_profile_health_cache(prof)
|
|
41
|
+
if wait_healthy:
|
|
42
|
+
_wait_profile_api_healthy(prof, attempts=48, delay=0.25)
|
|
43
|
+
return
|
|
44
|
+
_srv()._invalidate_profile_health_cache(prof)
|
|
45
|
+
try:
|
|
46
|
+
_srv().profile_gateway_lifecycle(prof, "stop", bootstrap_providers=False)
|
|
47
|
+
except ValueError:
|
|
48
|
+
pass
|
|
49
|
+
_srv().profile_gateway_lifecycle(prof, "start", bootstrap_providers=False)
|
|
50
|
+
if wait_healthy:
|
|
51
|
+
_wait_profile_api_healthy(prof)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _schedule_gateway_reload(profile: str) -> None:
|
|
55
|
+
"""ponytail: model-save hot path — yaml is sync; reload async (~7s, not ~45s)."""
|
|
56
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
57
|
+
if not _profile_api_healthy(prof, timeout=0.5, use_cache=False):
|
|
58
|
+
return # cold start on next bind via ensure_profile_api
|
|
59
|
+
|
|
60
|
+
def _run() -> None:
|
|
61
|
+
try:
|
|
62
|
+
with _srv()._gateway_lifecycle_lock:
|
|
63
|
+
_reload_runtime_profile_gateway(prof, wait_healthy=False)
|
|
64
|
+
except Exception as exc: # noqa: BLE001
|
|
65
|
+
print(f"[workframe-api] gateway reload failed for {prof}: {exc}")
|
|
66
|
+
|
|
67
|
+
threading.Thread(target=_run, name=f"gw-reload-{prof}", daemon=True).start()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _restart_runtime_profile_gateway(profile: str) -> None:
|
|
71
|
+
"""Hermes api_server caches config.yaml — reload after lease rotation."""
|
|
72
|
+
_reload_runtime_profile_gateway(profile, wait_healthy=True)
|
|
73
|
+
def _profile_api_port(profile: str) -> int:
|
|
74
|
+
if profile == _srv()._primary_profile():
|
|
75
|
+
return 8642
|
|
76
|
+
# Stable per-profile range: 18610..18709
|
|
77
|
+
base = 18610
|
|
78
|
+
span = 100
|
|
79
|
+
h = sum(ord(c) for c in profile) % span
|
|
80
|
+
return base + h
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _configure_profile_api(profile: str) -> tuple[bool, str, int]:
|
|
84
|
+
_srv()._normalize_profile_config_yaml(profile)
|
|
85
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
86
|
+
port = _profile_api_port(prof)
|
|
87
|
+
cfg_path = _srv()._profile_gateway_config_path(prof)
|
|
88
|
+
if cfg_path is None:
|
|
89
|
+
return False, f"profile not found: {prof}", port
|
|
90
|
+
try:
|
|
91
|
+
import yaml
|
|
92
|
+
|
|
93
|
+
cfg: dict[str, Any] = {}
|
|
94
|
+
if cfg_path.is_file():
|
|
95
|
+
loaded = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
|
|
96
|
+
cfg = loaded if isinstance(loaded, dict) else {}
|
|
97
|
+
plats = cfg.setdefault("platforms", {})
|
|
98
|
+
if not isinstance(plats, dict):
|
|
99
|
+
plats = {}
|
|
100
|
+
cfg["platforms"] = plats
|
|
101
|
+
native = prof == _srv()._primary_profile()
|
|
102
|
+
if not native:
|
|
103
|
+
api_only = plats.get("api_server") if isinstance(plats.get("api_server"), dict) else {}
|
|
104
|
+
plats = {"api_server": api_only}
|
|
105
|
+
cfg["platforms"] = plats
|
|
106
|
+
for name in ("discord", "telegram", "slack", "whatsapp", "webhook", "cron"):
|
|
107
|
+
plats[name] = {"enabled": False}
|
|
108
|
+
api = plats.setdefault("api_server", {})
|
|
109
|
+
if not isinstance(api, dict):
|
|
110
|
+
api = {}
|
|
111
|
+
plats["api_server"] = api
|
|
112
|
+
api["enabled"] = True
|
|
113
|
+
extra = api.setdefault("extra", {})
|
|
114
|
+
if not isinstance(extra, dict):
|
|
115
|
+
extra = {}
|
|
116
|
+
api["extra"] = extra
|
|
117
|
+
extra["host"] = "0.0.0.0"
|
|
118
|
+
extra["port"] = port
|
|
119
|
+
if not str(extra.get("key") or "").strip():
|
|
120
|
+
extra["key"] = "workframe-local-key"
|
|
121
|
+
cfg_path.write_text(yaml.safe_dump(cfg, sort_keys=False), encoding="utf-8")
|
|
122
|
+
return True, "ok", port
|
|
123
|
+
except (OSError, ImportError) as exc:
|
|
124
|
+
return False, str(exc), port
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _patch_profile_gateway_run_script(profile: str) -> tuple[bool, str]:
|
|
128
|
+
if profile == _srv()._primary_profile():
|
|
129
|
+
return True, "native profile unchanged"
|
|
130
|
+
profile_home = f"/opt/data/profiles/{profile}"
|
|
131
|
+
script = (
|
|
132
|
+
"from pathlib import Path\n"
|
|
133
|
+
f"p=Path('/run/service/gateway-{profile}/run')\n"
|
|
134
|
+
"if not p.exists():\n"
|
|
135
|
+
" print('missing run script')\n"
|
|
136
|
+
" raise SystemExit(1)\n"
|
|
137
|
+
"text=p.read_text(encoding='utf-8')\n"
|
|
138
|
+
"needle='export HERMES_S6_SUPERVISED_CHILD=1\\n'\n"
|
|
139
|
+
f"inject=('export HERMES_S6_SUPERVISED_CHILD=1\\n'"
|
|
140
|
+
f"'export HERMES_HOME={profile_home}\\n'"
|
|
141
|
+
f"'export HOME={profile_home}\\n'"
|
|
142
|
+
f"'cd {profile_home}\\n'"
|
|
143
|
+
"'if [ -z \"$WORKFRAME_PROXY_TOKEN\" ] && [ -f /run/workframe-proxy/token ]; then '\n"
|
|
144
|
+
"'export WORKFRAME_PROXY_TOKEN=\"$(tr -d \\'\\r\\n\\' < /run/workframe-proxy/token)\"; fi\\n'"
|
|
145
|
+
"'unset DISCORD_BOT_TOKEN DISCORD_ALLOWED_USERS DISCORD_HOME_CHANNEL\\n'"
|
|
146
|
+
"'unset TELEGRAM_BOT_TOKEN TELEGRAM_ALLOWED_USERS TELEGRAM_HOME_CHANNEL\\n'"
|
|
147
|
+
"'unset SLACK_BOT_TOKEN SLACK_APP_TOKEN WHATSAPP_MODE\\n'"
|
|
148
|
+
"'unset HERMES_DASHBOARD HERMES_DASHBOARD_HOST HERMES_DASHBOARD_PORT HERMES_DASHBOARD_INSECURE HERMES_DASHBOARD_TUI\\n')\n"
|
|
149
|
+
"if inject not in text:\n"
|
|
150
|
+
" text=text.replace(needle, inject)\n"
|
|
151
|
+
" p.write_text(text, encoding='utf-8')\n"
|
|
152
|
+
"print('ok')\n"
|
|
153
|
+
)
|
|
154
|
+
code, out = _srv()._gateway_container_exec(
|
|
155
|
+
["/opt/hermes/.venv/bin/python", "-c", script],
|
|
156
|
+
)
|
|
157
|
+
return code == 0, out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _disable_profile(prof: str) -> dict[str, Any]:
|
|
161
|
+
port = _profile_api_port(prof)
|
|
162
|
+
state = _srv().gateway_data(prof)
|
|
163
|
+
if str(state.get("state") or "").lower() in {"running", "starting"}:
|
|
164
|
+
code, out = _srv()._gateway_exec(prof, ["gateway", "stop"])
|
|
165
|
+
if code != 0:
|
|
166
|
+
raise ValueError(f"gateway stop failed: {out}")
|
|
167
|
+
script = (
|
|
168
|
+
"import yaml\n"
|
|
169
|
+
"from pathlib import Path\n"
|
|
170
|
+
f"d=Path('/opt/data/profiles/{prof}')\n"
|
|
171
|
+
"p=d/'config.yaml'\n"
|
|
172
|
+
"cfg=yaml.safe_load(p.read_text(encoding='utf-8')) if p.exists() else {}\n"
|
|
173
|
+
"plats=cfg.setdefault('platforms',{})\n"
|
|
174
|
+
"for name, value in list(plats.items()):\n"
|
|
175
|
+
" if isinstance(value, dict):\n"
|
|
176
|
+
" value['enabled']=False\n"
|
|
177
|
+
" else:\n"
|
|
178
|
+
" plats[name]={'enabled': False}\n"
|
|
179
|
+
"cfg['platforms']=plats\n"
|
|
180
|
+
"p.write_text(yaml.safe_dump(cfg, sort_keys=False), encoding='utf-8')\n"
|
|
181
|
+
f"Path('/opt/data/profiles/{prof}/.disabled').write_text('disabled by workframe-api\n', encoding='utf-8')\n"
|
|
182
|
+
"print('ok')\n"
|
|
183
|
+
)
|
|
184
|
+
code, out = _srv()._gateway_container_exec(
|
|
185
|
+
["/opt/hermes/.venv/bin/python", "-c", script],
|
|
186
|
+
)
|
|
187
|
+
if code != 0:
|
|
188
|
+
raise ValueError(f"disable profile config failed: {out}")
|
|
189
|
+
return {"ok": True, "profile": prof, "action": "disable", "state": "disabled", "api_port": port, "output": out.strip()}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def profile_gateway_lifecycle(profile: str, action: str, *, bootstrap_providers: bool = True) -> dict[str, Any]:
|
|
193
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
194
|
+
if action in {"start", "stop", "disable"}:
|
|
195
|
+
_srv()._invalidate_gateway_registered_cache(prof)
|
|
196
|
+
_srv()._invalidate_profile_health_cache(prof)
|
|
197
|
+
if action not in {"start", "stop", "status", "disable"}:
|
|
198
|
+
raise ValueError("invalid action")
|
|
199
|
+
port = _profile_api_port(prof)
|
|
200
|
+
if _srv().SECURE_MODE and prof != _srv()._primary_profile() and action in {"start", "stop", "disable"}:
|
|
201
|
+
if action == "start":
|
|
202
|
+
_srv()._normalize_profile_config_yaml(prof)
|
|
203
|
+
return _srv()._supervisor_profile_lifecycle(prof, action)
|
|
204
|
+
if prof == _srv()._primary_profile():
|
|
205
|
+
if action == "disable":
|
|
206
|
+
raise ValueError("cannot disable the native profile")
|
|
207
|
+
if action == "status":
|
|
208
|
+
state = _srv().gateway_data(prof)
|
|
209
|
+
ok = bool(state.get("ok")) and str(state.get("state") or "").lower() == "running"
|
|
210
|
+
return {
|
|
211
|
+
"ok": ok,
|
|
212
|
+
"profile": prof,
|
|
213
|
+
"action": action,
|
|
214
|
+
"api_port": port,
|
|
215
|
+
"state": state.get("state") or "unknown",
|
|
216
|
+
"details": state,
|
|
217
|
+
}
|
|
218
|
+
if action == "stop":
|
|
219
|
+
raise ValueError("native gateway stop is not supported via profile api lifecycle")
|
|
220
|
+
return {"ok": True, "profile": prof, "action": action, "api_port": port, "output": "native gateway already managed by compose"}
|
|
221
|
+
if action == "disable":
|
|
222
|
+
return _disable_profile(prof)
|
|
223
|
+
with _srv()._gateway_lifecycle_lock:
|
|
224
|
+
if action == "start":
|
|
225
|
+
if bootstrap_providers:
|
|
226
|
+
_srv()._bootstrap_profile_providers(prof)
|
|
227
|
+
ok, out, port = _configure_profile_api(prof)
|
|
228
|
+
if not ok:
|
|
229
|
+
raise ValueError(f"profile api config failed: {out}")
|
|
230
|
+
ok, out = _patch_profile_gateway_run_script(prof)
|
|
231
|
+
if not ok:
|
|
232
|
+
raise ValueError(f"profile api run patch failed: {out}")
|
|
233
|
+
state = _srv().gateway_data(prof)
|
|
234
|
+
if str(state.get("state") or "").lower() in {"running", "starting"} and not _profile_api_healthy(prof):
|
|
235
|
+
code, out = _srv()._gateway_exec(prof, ["gateway", "stop"])
|
|
236
|
+
if code != 0:
|
|
237
|
+
raise ValueError(f"gateway stop failed: {out}")
|
|
238
|
+
time.sleep(1.0)
|
|
239
|
+
if action == "status":
|
|
240
|
+
state = _srv().gateway_data(prof)
|
|
241
|
+
ok = bool(state.get("ok")) and str(state.get("state") or "").lower() == "running"
|
|
242
|
+
return {
|
|
243
|
+
"ok": ok,
|
|
244
|
+
"profile": prof,
|
|
245
|
+
"action": action,
|
|
246
|
+
"api_port": port,
|
|
247
|
+
"state": state.get("state") or "unknown",
|
|
248
|
+
"details": state,
|
|
249
|
+
}
|
|
250
|
+
code, out = _srv()._gateway_exec(prof, ["gateway", action])
|
|
251
|
+
if code != 0:
|
|
252
|
+
raise ValueError(f"gateway {action} failed: {out}")
|
|
253
|
+
if action == "start" and not _wait_profile_api_healthy(prof):
|
|
254
|
+
raise ValueError(f"profile api did not become healthy: {prof}")
|
|
255
|
+
return {"ok": True, "profile": prof, "action": action, "api_port": port, "output": out.strip()}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _profile_api_key(profile: str) -> str:
|
|
261
|
+
return "workframe-local-key"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _profile_turn_payload(profile: str, text: str, room_id: str = "") -> dict[str, Any]:
|
|
265
|
+
message = text
|
|
266
|
+
room_id = str(room_id or "").strip()
|
|
267
|
+
if room_id:
|
|
268
|
+
try:
|
|
269
|
+
conn = _srv()._workframe_db()
|
|
270
|
+
try:
|
|
271
|
+
room = conn.execute(
|
|
272
|
+
"""
|
|
273
|
+
SELECT room_type, agent_profile_id
|
|
274
|
+
FROM rooms WHERE id = ? AND deleted_at IS NULL
|
|
275
|
+
""",
|
|
276
|
+
(room_id,),
|
|
277
|
+
).fetchone()
|
|
278
|
+
if room and _srv()._is_space_room(str(room["room_type"]), room["agent_profile_id"]):
|
|
279
|
+
transcript = _srv()._room_recent_transcript(conn, room_id)
|
|
280
|
+
message = (
|
|
281
|
+
"You are in a group chat. Reply only to the message that @mentioned you.\n\n"
|
|
282
|
+
f"Recent messages:\n{transcript}\n\n"
|
|
283
|
+
"Respond concisely."
|
|
284
|
+
)
|
|
285
|
+
finally:
|
|
286
|
+
conn.close()
|
|
287
|
+
except Exception: # noqa: BLE001
|
|
288
|
+
pass
|
|
289
|
+
payload: dict[str, Any] = {"message": message}
|
|
290
|
+
soul = _srv()._profile_soul_text(profile)
|
|
291
|
+
if soul:
|
|
292
|
+
payload["instructions"] = soul
|
|
293
|
+
return payload
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _profile_api_request(
|
|
297
|
+
profile: str,
|
|
298
|
+
method: str,
|
|
299
|
+
path: str,
|
|
300
|
+
body: dict[str, Any] | None = None,
|
|
301
|
+
) -> tuple[int, Any]:
|
|
302
|
+
port = _profile_api_port(profile)
|
|
303
|
+
url = f"http://gateway:{port}{path}"
|
|
304
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
305
|
+
headers = {
|
|
306
|
+
"Authorization": f"Bearer {_profile_api_key(profile)}",
|
|
307
|
+
"Content-Type": "application/json",
|
|
308
|
+
}
|
|
309
|
+
last_error: Exception | None = None
|
|
310
|
+
for attempt in range(4):
|
|
311
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
312
|
+
try:
|
|
313
|
+
with urllib.request.urlopen(req, timeout=1800) as resp:
|
|
314
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
315
|
+
return int(resp.status), (json.loads(raw) if raw else {})
|
|
316
|
+
except urllib.error.HTTPError as exc:
|
|
317
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
318
|
+
try:
|
|
319
|
+
return int(exc.code), json.loads(raw)
|
|
320
|
+
except Exception: # noqa: BLE001
|
|
321
|
+
return int(exc.code), raw
|
|
322
|
+
except urllib.error.URLError as exc:
|
|
323
|
+
last_error = exc
|
|
324
|
+
if attempt < 3 and _is_transient_profile_api_error(exc):
|
|
325
|
+
time.sleep(0.35 * (attempt + 1))
|
|
326
|
+
continue
|
|
327
|
+
raise
|
|
328
|
+
if last_error:
|
|
329
|
+
raise last_error
|
|
330
|
+
return 503, {"error": "profile api unavailable"}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _is_transient_profile_api_error(exc: Exception) -> bool:
|
|
334
|
+
reason = str(getattr(exc, "reason", exc) or exc).lower()
|
|
335
|
+
return any(token in reason for token in ("connection refused", "timed out", "temporarily unavailable"))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _profile_api_healthy(profile: str, timeout: float = 1.5, *, use_cache: bool = True) -> bool:
|
|
339
|
+
try:
|
|
340
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
341
|
+
except ValueError:
|
|
342
|
+
return False
|
|
343
|
+
wait = max(1.0, float(timeout))
|
|
344
|
+
if use_cache and wait >= 1.0:
|
|
345
|
+
cached = _srv()._profile_health_cache.get(prof)
|
|
346
|
+
if cached and time.monotonic() - cached[1] < _srv()._PROFILE_HEALTH_TTL_SEC:
|
|
347
|
+
return cached[0]
|
|
348
|
+
port = _profile_api_port(prof)
|
|
349
|
+
key = _profile_api_key(prof)
|
|
350
|
+
url = f"http://gateway:{port}/v1/health"
|
|
351
|
+
req = urllib.request.Request(
|
|
352
|
+
url,
|
|
353
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
354
|
+
method="GET",
|
|
355
|
+
)
|
|
356
|
+
try:
|
|
357
|
+
with urllib.request.urlopen(req, timeout=wait) as resp:
|
|
358
|
+
ok = int(resp.status) < 300
|
|
359
|
+
except Exception: # noqa: BLE001
|
|
360
|
+
ok = False
|
|
361
|
+
if use_cache and wait >= 1.0:
|
|
362
|
+
_srv()._profile_health_cache[prof] = (ok, time.monotonic())
|
|
363
|
+
return ok
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _wait_profile_api_healthy(profile: str, attempts: int = 60, delay: float = 0.5) -> bool:
|
|
367
|
+
try:
|
|
368
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
369
|
+
except ValueError:
|
|
370
|
+
return False
|
|
371
|
+
if _profile_api_healthy(prof):
|
|
372
|
+
return True
|
|
373
|
+
for _ in range(attempts):
|
|
374
|
+
if _profile_api_healthy(prof):
|
|
375
|
+
return True
|
|
376
|
+
time.sleep(delay)
|
|
377
|
+
return False
|
|
378
|
+
def profile_gateway_stop(profile: str, run_id: str) -> dict[str, Any]:
|
|
379
|
+
"""Stop a running agent via the Hermes /v1/runs/{run_id}/stop API."""
|
|
380
|
+
prof = _srv().resolve_validated_profile(profile)
|
|
381
|
+
lifecycle = _srv().ensure_profile_api(prof)
|
|
382
|
+
port = _profile_api_port(prof)
|
|
383
|
+
api_base = f"http://gateway:{port}"
|
|
384
|
+
auth_headers = {
|
|
385
|
+
"Authorization": f"Bearer {_profile_api_key(prof)}",
|
|
386
|
+
"Content-Type": "application/json",
|
|
387
|
+
}
|
|
388
|
+
try:
|
|
389
|
+
req = urllib.request.Request(
|
|
390
|
+
f"{api_base}/v1/runs/{urllib.parse.quote(run_id, safe='')}/stop",
|
|
391
|
+
data=b"{}",
|
|
392
|
+
headers=auth_headers,
|
|
393
|
+
method="POST",
|
|
394
|
+
)
|
|
395
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
396
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
397
|
+
return {"ok": True, "profile": prof, "run_id": run_id, "status": "stopped", "detail": body[:500]}
|
|
398
|
+
except urllib.error.HTTPError as exc:
|
|
399
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
400
|
+
return {"ok": False, "profile": prof, "run_id": run_id, "error": raw or f"stop failed: {exc.code}"}
|
|
401
|
+
except Exception as exc:
|
|
402
|
+
return {"ok": False, "profile": prof, "run_id": run_id, "error": str(exc)}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def profile_gateway_steer(profile: str, run_id: str, text: str) -> dict[str, Any]:
|
|
406
|
+
"""Steer a running agent: stop the current run and append a new input via /v1/runs."""
|
|
407
|
+
prof = _srv().resolve_validated_profile(profile)
|
|
408
|
+
# First stop the current run
|
|
409
|
+
stop_result = _srv().profile_gateway_stop(prof, run_id)
|
|
410
|
+
if not stop_result.get("ok"):
|
|
411
|
+
return stop_result
|
|
412
|
+
# Then submit the steered input as a new run
|
|
413
|
+
lifecycle = _srv().ensure_profile_api(prof)
|
|
414
|
+
port = _profile_api_port(prof)
|
|
415
|
+
api_base = f"http://gateway:{port}"
|
|
416
|
+
auth_headers = {
|
|
417
|
+
"Authorization": f"Bearer {_profile_api_key(prof)}",
|
|
418
|
+
"Content-Type": "application/json",
|
|
419
|
+
}
|
|
420
|
+
payload = json.dumps({"input": text}).encode("utf-8")
|
|
421
|
+
try:
|
|
422
|
+
req = urllib.request.Request(
|
|
423
|
+
f"{api_base}/v1/runs",
|
|
424
|
+
data=payload,
|
|
425
|
+
headers=auth_headers,
|
|
426
|
+
method="POST",
|
|
427
|
+
)
|
|
428
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
429
|
+
body = json.loads(resp.read().decode("utf-8", errors="replace"))
|
|
430
|
+
new_run_id = str(body.get("run_id") or "").strip()
|
|
431
|
+
return {"ok": True, "profile": prof, "run_id": new_run_id, "message": "steered", "detail": str(body)[:500]}
|
|
432
|
+
except urllib.error.HTTPError as exc:
|
|
433
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
434
|
+
return {"ok": False, "profile": prof, "error": raw or f"steer failed: {exc.code}"}
|
|
435
|
+
except Exception as exc:
|
|
436
|
+
return {"ok": False, "profile": prof, "error": str(exc)}
|