create-workframe 0.1.14 → 0.1.15
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/sync-canonical-to-package.mjs +3 -0
- package/workframe-api/activity_feed.py +53 -5
- package/workframe-api/api_errors.py +126 -0
- package/workframe-api/chat_bind.py +8 -1
- package/workframe-api/chat_stream.py +70 -26
- package/workframe-api/crew_registry.py +3 -6
- package/workframe-api/google_auth.py +20 -12
- package/workframe-api/handler_modules/handler_auth.py +66 -21
- package/workframe-api/handler_modules/handler_chat.py +25 -0
- package/workframe-api/handler_modules/handler_install.py +45 -0
- package/workframe-api/handler_modules/handler_provider.py +1 -0
- package/workframe-api/handler_modules/handler_workspace.py +30 -16
- package/workframe-api/hermes_admin.py +9 -2
- package/workframe-api/hermes_profiles.py +25 -5
- package/workframe-api/install_api.py +38 -4
- package/workframe-api/lane_bindings.py +24 -0
- package/workframe-api/llm_error_glossary.py +58 -4
- package/workframe-api/mention_invoke.py +89 -27
- package/workframe-api/model_surface.py +1 -1
- package/workframe-api/provider_bindings.py +44 -2
- package/workframe-api/provider_bootstrap.py +9 -0
- package/workframe-api/provider_catalog.py +3 -3
- package/workframe-api/rooms.py +3 -0
- package/workframe-api/route_registry.py +1 -0
- package/workframe-api/run_ledger.py +52 -8
- package/workframe-api/runtime_cohort.py +11 -1
- package/workframe-api/server.py +25 -1
- package/workframe-api/stack_config.py +4 -1
- package/workframe-api/test_api_errors.py +33 -0
- package/workframe-api/test_enrich_room_messages.py +72 -0
- package/workframe-api/test_exception_hygiene.py +4 -2
- package/workframe-api/test_hermes_admin.py +7 -0
- package/workframe-api/test_install_wizard_resume.py +39 -13
- package/workframe-api/test_model_surface_consistency.py +118 -100
- package/workframe-api/test_profile_model_yaml.py +4 -1
- package/workframe-api/test_provider_bindings.py +10 -0
- package/workframe-api/test_run_ledger.py +2 -1
- package/workframe-api/test_server_reexports.py +4 -0
- package/workframe-api/test_user_llm_prefs.py +99 -0
- package/workframe-api/time-overlay-split.py +58 -0
- package/workframe-api/time-ttft-quick.py +135 -0
- package/workframe-api/time-ttft.py +250 -0
- package/workframe-api/time-wf-stream.py +145 -0
- package/workframe-api/turn_overlay.py +96 -23
- package/workframe-api/updates.py +1 -2
- package/workframe-api/user_prefs.py +26 -5
- package/workframe-api/workspace_bootstrap.py +16 -10
- package/workframe-ui/public/assets/{arc-BT9iZUSd.js → arc-BARqpbNX.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CYE89P6J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-CaEDd3np.js → architectureDiagram-3BPJPVTR-bbpW6hfw.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DHJhMrIS.js → blockDiagram-GPEHLZMM-aG0oR9eA.js} +1 -1
- package/workframe-ui/public/assets/brave-B885JYnH.svg +11 -0
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Cdyz9hIV.js → c4Diagram-AAUBKEIU-BDi4E5jK.js} +1 -1
- package/workframe-ui/public/assets/channel-B4ex0Z2H.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-BbZx76LX.js → chunk-2J33WTMH-BBU5tRNe.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DcSxjTIP.js → chunk-3OPIFGDE-SKhe5YD8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-B7F-kTY1.js → chunk-4BX2VUAB-D4uKMCPc.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-BwwEiRTK.js → chunk-55IACEB6-SnqJA0wG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DAi_NksX.js → chunk-5ZQYHXKU-ZyBWNsT2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BIaF4XCN.js → chunk-727SXJPM-DbxwKsiK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DjYSk1dG.js → chunk-AQP2D5EJ-Bm2iWcL0.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-_twnyfgV.js → chunk-BSJP7CBP-CGwyQh1d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CkNmdy5s.js → chunk-CSCIHK7Q-XWSO96VW.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CjLqd-gl.js → chunk-FMBD7UC4-D0OTpUqL.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-_6oNqFjj.js → chunk-KSCS5N6A-CM75wJ6j.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CeO9Am_D.js → chunk-L5ZTLDWV-BCgtwaGP.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-sRM4YeIe.js → chunk-LZXEDZCA-CZBc86Td.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CH8tMb21.js → chunk-ND2GUHAM-DIq9-Qtg.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-DAI1bsII.js → chunk-NZK2D7GU-BTOOFYOR.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B2e-5a2G.js → chunk-O5CBEL6O-CYriwTot.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-sPK7umZx.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DtLDSmiK.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-DM2-0q37.js → chunk-XPW4576I-6iEhdiBi.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DWLy8pDS.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DWLy8pDS.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-BnbbewLv.js → cose-bilkent-S5V4N54A-C716vjLB.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-C4zmB1Vt.js → dagre-BM42HDAG-Cs-MNVkM.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-n-BJmsiC.js → diagram-2AECGRRQ-BqoyTWyW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzQGQnZi.js → diagram-5GNKFQAL-CDRTV5fY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Cx_De-wS.js → diagram-KO2AKTUF-u9NjixEi.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CIOI3Lok.js → diagram-LMA3HP47-rDd6T2os.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-DkRuPLWY.js → diagram-OG6HWLK6-Bl4VX9RO.js} +1 -1
- package/workframe-ui/public/assets/{dist-XhCT1Q-V.js → dist-DSCcbFgV.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-C3GwhDU1.js → erDiagram-TEJ5UH35-C9Nv-5nl.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-B7kChV1b.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CLWH8z2e.js → flowDiagram-I6XJVG4X-D8Xhbd7s.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-D4h66Vy3.js → ganttDiagram-6RSMTGT7-CMycBZqm.js} +1 -1
- package/workframe-ui/public/assets/gemini-Cq-0-rmr.svg +105 -0
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-IW5F-Q6c.js → gitGraph-WXDBUCRP-DnIro8tZ.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BjNIbmRM.js → gitGraphDiagram-PVQCEYII-B74H9ISZ.js} +1 -1
- package/workframe-ui/public/assets/index-BLVAsAeO.css +1 -0
- package/workframe-ui/public/assets/index-s4167RRh.js +132 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CcJspM79.js → info-J43DQDTF-B6mVw1B6.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-DP4xR9jt.js → infoDiagram-5YYISTIA-Ddtfq_xy.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-D4cZb21V.js → ishikawaDiagram-YF4QCWOH-DBb6veAL.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DsLuHkIJ.js → journeyDiagram-JHISSGLW-DMgdeS5h.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Wu65nOsR.js → kanban-definition-UN3LZRKU-DiV_LoDN.js} +1 -1
- package/workframe-ui/public/assets/{line-ByBDzRpy.js → line-BR9uEes_.js} +1 -1
- package/workframe-ui/public/assets/{linear-DzXi2vnq.js → linear-BhjkJfy9.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CaB-1Ckn.js → mermaid-parser.core-CEWtaJIq.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-D7HTHC17.js → mermaid.core-B6xKLKp-.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BfKsC9aj.js → mindmap-definition-RKZ34NQL-DypZm9OU.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-ClpU98TO.js → packet-YPE3B663-CCxb0WDS.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-KeOyY_-u.js → pie-LRSECV5Y-pTkr4g8B.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CUpjnzbs.js → pieDiagram-4H26LBE5-BventgTb.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CJdPTszF.js → quadrantDiagram-W4KKPZXB-BQnvFJYk.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C9BpandZ.js → radar-GUYGQ44K-EX9Pzmli.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-CHDZPwry.js → requirementDiagram-4Y6WPE33-Bnw4xecM.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-CMJJh91s.js → sankeyDiagram-5OEKKPKP-spHh-EYp.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BsV4GppP.js → sequenceDiagram-3UESZ5HK-CC97Qcbf.js} +1 -1
- package/workframe-ui/public/assets/{src-WHks82Up.js → src-DRcBNyeZ.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-eSX9P8z0.js → stateDiagram-AJRCARHV-C8r8rx81.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CnedwxVT.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-Dg1UkhGo.js → timeline-definition-PNZ67QCA-CA4JxR_w.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-B7yGsBuj.js → treeView-BLDUP644-BuGSo6xP.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-j_G2oyQ_.js → treemap-LRROVOQU-JyJMArE9.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BRrT0jqC.js → vennDiagram-CIIHVFJN-BZAUo7xL.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-DEdUqez2.js → wardley-L42UT6IY-CMgm8t9M.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-C-bBK9Bb.js → wardleyDiagram-YWT4CUSO-CqI1c2zN.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DhH5xuR2.js → xychartDiagram-2RQKCTM6-DhTdH-jJ.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/workframe-build.json +3 -3
- package/docs/security.md +0 -85
- package/workframe-ui/public/assets/architecture-7EHR7CIX-m1aPUQ_r.js +0 -1
- package/workframe-ui/public/assets/channel-3M69ekE5.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BMl9ed8P.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-C08HH6BU.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BFcOE4Aq.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BFcOE4Aq.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DIVBL6CF.js +0 -1
- package/workframe-ui/public/assets/index-DL2vJP4L.css +0 -1
- package/workframe-ui/public/assets/index-ElS_D59P.js +0 -130
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-FzMK4PkM.js +0 -1
|
@@ -16,6 +16,7 @@ from typing import Any
|
|
|
16
16
|
import auth_rate_limit
|
|
17
17
|
import google_auth
|
|
18
18
|
import stack_config
|
|
19
|
+
import zk_auth as _zk
|
|
19
20
|
from email_sender import APP_BASE_URL
|
|
20
21
|
|
|
21
22
|
|
|
@@ -188,6 +189,13 @@ class AuthRoutesMixin:
|
|
|
188
189
|
self._log_audit("login_denied_private", "user", email, str(deny_meta.get("error") or ""))
|
|
189
190
|
self._json(403, {"ok": False, **deny_meta})
|
|
190
191
|
return
|
|
192
|
+
import install_api
|
|
193
|
+
|
|
194
|
+
install_allowed, install_deny = install_api.install_auth_email_allowed(email)
|
|
195
|
+
if not install_allowed:
|
|
196
|
+
self._log_audit("login_denied_install", "user", email, str(install_deny.get("error") or ""))
|
|
197
|
+
self._json(403, {"ok": False, **install_deny})
|
|
198
|
+
return
|
|
191
199
|
try:
|
|
192
200
|
base = APP_BASE_URL.strip().lower()
|
|
193
201
|
loopback = base.startswith("http://127.0.0.1") or base.startswith("http://localhost")
|
|
@@ -230,6 +238,13 @@ class AuthRoutesMixin:
|
|
|
230
238
|
self._log_audit("login_denied_private", "user", email, str(deny_meta.get("error") or ""))
|
|
231
239
|
self._json(403, {"ok": False, **deny_meta})
|
|
232
240
|
return
|
|
241
|
+
import install_api
|
|
242
|
+
|
|
243
|
+
install_allowed, install_deny = install_api.install_auth_email_allowed(email)
|
|
244
|
+
if not install_allowed:
|
|
245
|
+
self._log_audit("login_denied_install", "user", email, str(install_deny.get("error") or ""))
|
|
246
|
+
self._json(403, {"ok": False, **install_deny})
|
|
247
|
+
return
|
|
233
248
|
try:
|
|
234
249
|
result = _zk.verify_email_code(email, code)
|
|
235
250
|
except ValueError as exc:
|
|
@@ -246,6 +261,28 @@ class AuthRoutesMixin:
|
|
|
246
261
|
self._ensure_user(result["user_id"], email, email)
|
|
247
262
|
if srv._install_window_open():
|
|
248
263
|
stack_config.mark_install_admin_verified(email)
|
|
264
|
+
try:
|
|
265
|
+
conn = srv._workframe_db()
|
|
266
|
+
ws = conn.execute(
|
|
267
|
+
"""
|
|
268
|
+
SELECT id FROM workspaces
|
|
269
|
+
WHERE slug = 'default' AND deleted_at IS NULL
|
|
270
|
+
LIMIT 1
|
|
271
|
+
""",
|
|
272
|
+
).fetchone()
|
|
273
|
+
if ws:
|
|
274
|
+
ws_id = str(ws["id"])
|
|
275
|
+
srv._promote_workspace_owner_if_unclaimed(conn, ws_id, result["user_id"])
|
|
276
|
+
conn.execute(
|
|
277
|
+
"UPDATE users SET role = 'owner', updated_at = ? WHERE id = ?",
|
|
278
|
+
(str(int(time.time())), result["user_id"]),
|
|
279
|
+
)
|
|
280
|
+
conn.commit()
|
|
281
|
+
conn.close()
|
|
282
|
+
if not ws:
|
|
283
|
+
self._first_owner_bootstrap(result["user_id"], email, email)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
249
286
|
self._log_audit("login", "session", result["session_id"],
|
|
250
287
|
f"user={result['user_id']} new={result['is_new_user']}")
|
|
251
288
|
me_payload = srv._session_profile_payload(result["user_id"])
|
|
@@ -384,53 +421,58 @@ class AuthRoutesMixin:
|
|
|
384
421
|
srv._log_handler_error("GET /api/auth/google/callback", exc)
|
|
385
422
|
self._json(401, {"ok": False, "error": str(exc)})
|
|
386
423
|
|
|
424
|
+
def _redirect_provider_connect(self, provider: str, *, ok: bool, message: str = "") -> None:
|
|
425
|
+
status = "ok" if ok else "error"
|
|
426
|
+
detail = message or ("connected" if ok else "failed")
|
|
427
|
+
location = (
|
|
428
|
+
f"{APP_BASE_URL.rstrip('/')}/?provider_connect={urllib.parse.quote(provider)}"
|
|
429
|
+
f"&status={status}&message={urllib.parse.quote(detail)}"
|
|
430
|
+
)
|
|
431
|
+
self.send_response(302)
|
|
432
|
+
self.send_header("Location", location)
|
|
433
|
+
self.end_headers()
|
|
434
|
+
|
|
387
435
|
def _route_get_oauth_github_callback(self, qs: dict[str, list[str]]) -> None:
|
|
388
436
|
srv = _srv()
|
|
389
437
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
390
438
|
if not user_id:
|
|
391
|
-
self.
|
|
439
|
+
self._redirect_provider_connect("github", ok=False, message="no_session")
|
|
392
440
|
return
|
|
393
441
|
code = qs.get("code", [""])[0]
|
|
394
442
|
state = qs.get("state", [""])[0]
|
|
395
443
|
result = srv._complete_github_oauth(user_id, str(code or ""), str(state or ""))
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
self.
|
|
400
|
-
self.send_header("Location", location)
|
|
401
|
-
self.end_headers()
|
|
444
|
+
if result.get("ok"):
|
|
445
|
+
self._redirect_provider_connect("github", ok=True)
|
|
446
|
+
return
|
|
447
|
+
self._redirect_provider_connect("github", ok=False, message=str(result.get("error") or "failed"))
|
|
402
448
|
|
|
403
449
|
def _route_get_oauth_discord_callback(self, qs: dict[str, list[str]]) -> None:
|
|
404
450
|
srv = _srv()
|
|
405
451
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
406
452
|
if not user_id:
|
|
407
|
-
self.
|
|
453
|
+
self._redirect_provider_connect("discord", ok=False, message="no_session")
|
|
408
454
|
return
|
|
409
455
|
code = qs.get("code", [""])[0]
|
|
410
456
|
state = qs.get("state", [""])[0]
|
|
411
457
|
result = srv._complete_discord_oauth(user_id, str(code or ""), str(state or ""))
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
self.
|
|
416
|
-
self.send_header("Location", location)
|
|
417
|
-
self.end_headers()
|
|
458
|
+
if result.get("ok"):
|
|
459
|
+
self._redirect_provider_connect("discord", ok=True)
|
|
460
|
+
return
|
|
461
|
+
self._redirect_provider_connect("discord", ok=False, message=str(result.get("error") or "failed"))
|
|
418
462
|
|
|
419
463
|
def _route_get_oauth_stripe_callback(self, qs: dict[str, list[str]]) -> None:
|
|
420
464
|
srv = _srv()
|
|
421
465
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
422
466
|
if not user_id:
|
|
423
|
-
self.
|
|
467
|
+
self._redirect_provider_connect("stripe", ok=False, message="no_session")
|
|
424
468
|
return
|
|
425
469
|
code = qs.get("code", [""])[0]
|
|
426
470
|
state = qs.get("state", [""])[0]
|
|
427
471
|
result = srv._complete_stripe_oauth(user_id, str(code or ""), str(state or ""))
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
self.
|
|
432
|
-
self.send_header("Location", location)
|
|
433
|
-
self.end_headers()
|
|
472
|
+
if result.get("ok"):
|
|
473
|
+
self._redirect_provider_connect("stripe", ok=True)
|
|
474
|
+
return
|
|
475
|
+
self._redirect_provider_connect("stripe", ok=False, message=str(result.get("error") or "failed"))
|
|
434
476
|
|
|
435
477
|
def _route_get_me(self, qs: dict[str, list[str]]) -> None:
|
|
436
478
|
srv = _srv()
|
|
@@ -475,6 +517,9 @@ class AuthRoutesMixin:
|
|
|
475
517
|
if not user_id:
|
|
476
518
|
self._json(401, {"ok": False, "error": "no_session"})
|
|
477
519
|
return
|
|
520
|
+
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
521
|
+
self._json(403, {"ok": False, "error": "native_agent_identity_owner_only"})
|
|
522
|
+
return
|
|
478
523
|
native_slug = str(srv.NATIVE_PROFILE or "workframe-agent")
|
|
479
524
|
runtime = srv._runtime_profile_slug(user_id, native_slug)
|
|
480
525
|
soul = (
|
|
@@ -223,6 +223,17 @@ class ChatRoutesMixin:
|
|
|
223
223
|
self._json(400, {"error": "model required"})
|
|
224
224
|
return
|
|
225
225
|
billing_provider = str(body.get("billing_provider", "")).strip()
|
|
226
|
+
# A member may tune the model used by their proxy, but cannot mutate
|
|
227
|
+
# the shared agent template. The template remains the default model
|
|
228
|
+
# owned by the agent; this maps ordinary member writes to u-* runtime.
|
|
229
|
+
if profile and not selection_only and not srv._is_runtime_profile_slug(profile):
|
|
230
|
+
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
231
|
+
if not workspace_id:
|
|
232
|
+
workspaces = srv._get_user_workspaces(user_id)
|
|
233
|
+
current = srv._resolve_current_workspace(user_id, workspaces)
|
|
234
|
+
workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
|
|
235
|
+
profile = srv._runtime_profile_slug(user_id, profile)
|
|
236
|
+
srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
|
|
226
237
|
self._json(
|
|
227
238
|
200,
|
|
228
239
|
hermes_model_set(
|
|
@@ -256,6 +267,14 @@ class ChatRoutesMixin:
|
|
|
256
267
|
chain = body.get("chain", [])
|
|
257
268
|
selection_only = bool(body.get("selection_only"))
|
|
258
269
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
270
|
+
if profile and not selection_only and not srv._is_runtime_profile_slug(profile):
|
|
271
|
+
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
272
|
+
if not workspace_id:
|
|
273
|
+
workspaces = srv._get_user_workspaces(user_id)
|
|
274
|
+
current = srv._resolve_current_workspace(user_id, workspaces)
|
|
275
|
+
workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
|
|
276
|
+
profile = srv._runtime_profile_slug(user_id, profile)
|
|
277
|
+
srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
|
|
259
278
|
if not isinstance(chain, list):
|
|
260
279
|
self._json(400, {"error": "chain must be a list"})
|
|
261
280
|
return
|
|
@@ -417,6 +436,9 @@ class ChatRoutesMixin:
|
|
|
417
436
|
self._json(400, {"error": "name required"})
|
|
418
437
|
return
|
|
419
438
|
model = str(body.get("model") or "").strip()
|
|
439
|
+
if not model:
|
|
440
|
+
self._json(400, {"error": "model_required"})
|
|
441
|
+
return
|
|
420
442
|
description = str(body.get("description") or "").strip()
|
|
421
443
|
clone_from = str(body.get("clone_from") or "").strip()
|
|
422
444
|
soul = str(body.get("soul") or "").strip()
|
|
@@ -524,6 +546,9 @@ class ChatRoutesMixin:
|
|
|
524
546
|
return
|
|
525
547
|
template = resolve_validated_profile(profile_bootstrap_post.group(1))
|
|
526
548
|
user_id = str(getattr(self, "auth_user", "") or "").strip()
|
|
549
|
+
if srv._is_native_profile(template) and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
550
|
+
self._json(403, {"ok": False, "error": "native_agent_identity_owner_only"})
|
|
551
|
+
return
|
|
527
552
|
workspace_id = str(body.get("workspace_id") or "").strip()
|
|
528
553
|
if not workspace_id and user_id:
|
|
529
554
|
workspaces = srv._get_user_workspaces(user_id)
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import sqlite3
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
import install_api
|
|
9
10
|
import site_meta
|
|
10
11
|
import stack_config
|
|
12
|
+
import zk_auth as _zk
|
|
11
13
|
from supervisor_client import _maybe_sync_compose_public_url, _supervisor_ready, _supervisor_request
|
|
12
14
|
|
|
13
15
|
|
|
@@ -58,6 +60,49 @@ class InstallRoutesMixin:
|
|
|
58
60
|
url = (qs.get("url") or [""])[0] or stack_config.get_stack_config().get("app_base_url") or ""
|
|
59
61
|
self._json(200, install_api.publish_hints_payload(str(url)))
|
|
60
62
|
|
|
63
|
+
def _route_post_install_register_admin(self, body: dict) -> None:
|
|
64
|
+
srv = _srv()
|
|
65
|
+
if not srv._install_window_open():
|
|
66
|
+
self._json(403, {"ok": False, "error": "install_closed"})
|
|
67
|
+
return
|
|
68
|
+
if stack_config.install_admin_verified():
|
|
69
|
+
self._json(409, {"ok": False, "error": "already_verified"})
|
|
70
|
+
return
|
|
71
|
+
email = str(body.get("email") or "").strip().lower()
|
|
72
|
+
if not email or "@" not in email:
|
|
73
|
+
self._json(400, {"ok": False, "error": "email_required"})
|
|
74
|
+
return
|
|
75
|
+
display = str(body.get("display_name") or "").strip() or email.split("@", 1)[0]
|
|
76
|
+
stack_config.patch_stack_config({"smtp": {"admin_email": email}, "wizard_step": "welcome"})
|
|
77
|
+
allowed, deny_meta = install_api.install_auth_email_allowed(email)
|
|
78
|
+
if not allowed:
|
|
79
|
+
self._json(403, {"ok": False, **deny_meta})
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
result = _zk.create_session_for_email(email)
|
|
83
|
+
except (RuntimeError, OSError, sqlite3.Error) as exc:
|
|
84
|
+
srv._log_handler_error("POST /api/install/register-admin", exc)
|
|
85
|
+
self._json(500, {"ok": False, "error": str(exc)})
|
|
86
|
+
return
|
|
87
|
+
user_id = str(result["user_id"])
|
|
88
|
+
self.auth_user = user_id
|
|
89
|
+
self._ensure_user(user_id, display, email)
|
|
90
|
+
self._first_owner_bootstrap(user_id, display, email)
|
|
91
|
+
use_secure = srv._session_cookie_secure()
|
|
92
|
+
cookie_val = _zk.session_cookie_value(result["session_id"], secure=use_secure)
|
|
93
|
+
me_payload = srv._session_profile_payload(user_id)
|
|
94
|
+
self._json(
|
|
95
|
+
200,
|
|
96
|
+
{
|
|
97
|
+
"ok": True,
|
|
98
|
+
"user_id": user_id,
|
|
99
|
+
"session_id": result["session_id"],
|
|
100
|
+
"refresh_token": result["refresh_token"],
|
|
101
|
+
**(me_payload or {}),
|
|
102
|
+
},
|
|
103
|
+
extra_headers=[("Set-Cookie", cookie_val)],
|
|
104
|
+
)
|
|
105
|
+
|
|
61
106
|
def _route_post_install_email_test(self, body: dict) -> None:
|
|
62
107
|
srv = _srv()
|
|
63
108
|
if not srv._install_window_open():
|
|
@@ -190,6 +190,7 @@ class ProviderRoutesMixin:
|
|
|
190
190
|
except sqlite3.Error as exc:
|
|
191
191
|
self._json(500, {"ok": False, "error": f"db_error: {exc}"})
|
|
192
192
|
return
|
|
193
|
+
srv._invalidate_user_llm_picker_cache(user_id)
|
|
193
194
|
self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
|
|
194
195
|
health: dict[str, Any] = {}
|
|
195
196
|
if provider == "openrouter":
|
|
@@ -13,6 +13,7 @@ import urllib.parse
|
|
|
13
13
|
import uuid
|
|
14
14
|
|
|
15
15
|
import activity_feed
|
|
16
|
+
import api_errors
|
|
16
17
|
import chat_bind
|
|
17
18
|
from email_sender import APP_BASE_URL, send_branded_invite_email
|
|
18
19
|
|
|
@@ -402,14 +403,13 @@ class WorkspaceRoutesMixin:
|
|
|
402
403
|
return
|
|
403
404
|
try:
|
|
404
405
|
self._json(200, srv.room_chat_bind(rid, body, user_id))
|
|
405
|
-
except ValueError as exc:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
self._json(400, {"ok": False, "error": err})
|
|
406
|
+
except (ValueError, OSError) as exc:
|
|
407
|
+
payload = api_errors.public_api_error_payload(exc, context="room_bind")
|
|
408
|
+
status = api_errors.http_status_for_code(str(payload.get("error") or ""))
|
|
409
|
+
self._json(status, {"ok": False, **payload})
|
|
410
|
+
except Exception as exc: # noqa: BLE001 — urllib.URLError etc.
|
|
411
|
+
payload = api_errors.public_api_error_payload(exc, context="room_bind")
|
|
412
|
+
self._json(500, {"ok": False, **payload})
|
|
413
413
|
|
|
414
414
|
def _route_pattern_post_room_sessions_activate(self, path: str, body: dict) -> None:
|
|
415
415
|
srv = _srv()
|
|
@@ -437,13 +437,9 @@ class WorkspaceRoutesMixin:
|
|
|
437
437
|
binding_version=binding_version,
|
|
438
438
|
)
|
|
439
439
|
except ValueError as exc:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
elif "denied" in err:
|
|
444
|
-
self._json(403, {"ok": False, "error": err})
|
|
445
|
-
else:
|
|
446
|
-
self._json(400, {"ok": False, "error": err})
|
|
440
|
+
payload = api_errors.public_api_error_payload(exc, context="room_session_activate")
|
|
441
|
+
status = api_errors.http_status_for_code(str(payload.get("error") or ""))
|
|
442
|
+
self._json(status, {"ok": False, **payload})
|
|
447
443
|
return
|
|
448
444
|
self._json(200, payload)
|
|
449
445
|
|
|
@@ -1024,6 +1020,13 @@ class WorkspaceRoutesMixin:
|
|
|
1024
1020
|
conn.close()
|
|
1025
1021
|
self._json(404, {"ok": False, "error": "invite_not_found_or_expired"})
|
|
1026
1022
|
return
|
|
1023
|
+
invite_email = str(inv["email"] or "").strip().lower()
|
|
1024
|
+
user_row = conn.execute("SELECT email FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
1025
|
+
user_email = str(user_row["email"] or "").strip().lower() if user_row else ""
|
|
1026
|
+
if not user_email or user_email != invite_email:
|
|
1027
|
+
conn.close()
|
|
1028
|
+
self._json(403, {"ok": False, "error": "invite_email_mismatch"})
|
|
1029
|
+
return
|
|
1027
1030
|
if int(inv["expires_at"]) < int(time.time()):
|
|
1028
1031
|
conn.close()
|
|
1029
1032
|
self._json(410, {"ok": False, "error": "invite_expired"})
|
|
@@ -1048,6 +1051,15 @@ class WorkspaceRoutesMixin:
|
|
|
1048
1051
|
(user_id, now_ts, now_ts, inv["id"]))
|
|
1049
1052
|
conn.commit()
|
|
1050
1053
|
conn.close()
|
|
1054
|
+
# Re-acceptance is also a repair path. Do not leave an existing
|
|
1055
|
+
# member without the per-user Hermes proxy runtimes.
|
|
1056
|
+
try:
|
|
1057
|
+
if not srv._provision_invited_member_agent_runtimes(wid, user_id):
|
|
1058
|
+
self._json(500, {"ok": False, "error": "runtime_provision_failed"})
|
|
1059
|
+
return
|
|
1060
|
+
except Exception as exc: # noqa: BLE001
|
|
1061
|
+
self._json(500, {"ok": False, "error": f"runtime_provision_failed: {exc}"})
|
|
1062
|
+
return
|
|
1051
1063
|
srv._set_user_current_workspace(user_id, wid)
|
|
1052
1064
|
self._json(200, {"ok": True, "already_member": True, "workspace_id": wid, "room": room_join, "dm_room": dm_room})
|
|
1053
1065
|
return
|
|
@@ -1069,7 +1081,9 @@ class WorkspaceRoutesMixin:
|
|
|
1069
1081
|
except sqlite3.Error as exc:
|
|
1070
1082
|
self._json(500, {"error": f"db_error: {exc}"})
|
|
1071
1083
|
return
|
|
1072
|
-
srv._provision_invited_member_agent_runtimes(wid, user_id)
|
|
1084
|
+
if not srv._provision_invited_member_agent_runtimes(wid, user_id):
|
|
1085
|
+
self._json(500, {"ok": False, "error": "runtime_provision_failed"})
|
|
1086
|
+
return
|
|
1073
1087
|
srv._set_user_current_workspace(user_id, wid)
|
|
1074
1088
|
self._log_audit("invite_accepted", "workspace_invite", inv["id"], f"workspace={wid} user={user_id}")
|
|
1075
1089
|
self._json(
|
|
@@ -232,7 +232,7 @@ def _resolve_command(token: str) -> dict[str, Any] | None:
|
|
|
232
232
|
return None
|
|
233
233
|
needle = token if token.startswith("/") else f"/{token}"
|
|
234
234
|
for entry in HERMES_COMMANDS:
|
|
235
|
-
if entry.get("wip", False):
|
|
235
|
+
if entry.get("wip", False) or entry.get("dispatch") in {"gateway", "client:noOp"}:
|
|
236
236
|
continue
|
|
237
237
|
if entry["name"] == needle or needle in entry.get("aliases", []):
|
|
238
238
|
return entry
|
|
@@ -653,7 +653,14 @@ def hermes_commands_catalog() -> dict[str, Any]:
|
|
|
653
653
|
they aren't returned here. The UI never sees a half-implemented
|
|
654
654
|
command; either it's wired end-to-end or it's invisible.
|
|
655
655
|
"""
|
|
656
|
-
|
|
656
|
+
# One-shot gateway execution is not equivalent to Hermes' interactive TUI
|
|
657
|
+
# command loop. Only expose commands with a native Workframe handler; add a
|
|
658
|
+
# gateway command after an end-to-end test proves its behavior here.
|
|
659
|
+
visible = [
|
|
660
|
+
entry for entry in HERMES_COMMANDS
|
|
661
|
+
if not entry.get("wip", False)
|
|
662
|
+
and entry.get("dispatch") not in {"gateway", "client:noOp"}
|
|
663
|
+
]
|
|
657
664
|
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
658
665
|
for entry in visible:
|
|
659
666
|
grouped.setdefault(entry["category"], []).append(
|
|
@@ -53,19 +53,31 @@ def _hermes_data_uid_gid() -> tuple[int, int]:
|
|
|
53
53
|
try:
|
|
54
54
|
st = _srv().HERMES_DATA.stat()
|
|
55
55
|
return int(st.st_uid), int(st.st_gid)
|
|
56
|
-
except OSError:
|
|
56
|
+
except (AttributeError, OSError):
|
|
57
57
|
return 10000, 10000
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _ensure_profiles_dir_ready() -> None:
|
|
61
|
+
"""Gateway creates runtime profiles — parent dir must be writable by hermes uid."""
|
|
62
|
+
profiles = _srv().HERMES_DATA / "profiles"
|
|
63
|
+
profiles.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
uid, gid = _hermes_data_uid_gid()
|
|
65
|
+
try:
|
|
66
|
+
os.chown(profiles, uid, gid)
|
|
67
|
+
except (AttributeError, OSError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
60
71
|
def _chown_profile_tree(prof_dir: Path) -> None:
|
|
61
72
|
"""Gateway runs as the Agents volume owner — seeded files must match."""
|
|
73
|
+
_ensure_profiles_dir_ready()
|
|
62
74
|
uid, gid = _hermes_data_uid_gid()
|
|
63
75
|
prof_dir.mkdir(parents=True, exist_ok=True)
|
|
64
76
|
(prof_dir / "logs").mkdir(parents=True, exist_ok=True)
|
|
65
77
|
for path in [prof_dir, *prof_dir.rglob("*")]:
|
|
66
78
|
try:
|
|
67
79
|
os.chown(path, uid, gid)
|
|
68
|
-
except OSError:
|
|
80
|
+
except (AttributeError, OSError):
|
|
69
81
|
pass
|
|
70
82
|
|
|
71
83
|
|
|
@@ -461,9 +473,13 @@ def _profile_identity_overlay(profile: str) -> str:
|
|
|
461
473
|
prof = _srv().safe_profile_slug(profile)
|
|
462
474
|
if _srv()._is_runtime_profile_slug(prof):
|
|
463
475
|
template = _runtime_template_slug(prof)
|
|
464
|
-
|
|
476
|
+
# Keep runtime-local instructions separate from shared display
|
|
477
|
+
# identity. Name/role/tagline/avatar must come from the template.
|
|
478
|
+
reg = _srv()._agent_registry_row(template)
|
|
479
|
+
runtime_reg = _srv()._agent_registry_row(prof)
|
|
465
480
|
else:
|
|
466
481
|
reg = _srv()._agent_registry_row(prof)
|
|
482
|
+
runtime_reg = {}
|
|
467
483
|
blocks: list[str] = []
|
|
468
484
|
identity_lines: list[str] = []
|
|
469
485
|
display = str(reg.get("display_name") or "").strip()
|
|
@@ -477,7 +493,7 @@ def _profile_identity_overlay(profile: str) -> str:
|
|
|
477
493
|
identity_lines.append(f"- **Tagline:** {tagline}")
|
|
478
494
|
if identity_lines:
|
|
479
495
|
blocks.append("## User identity\n" + "\n".join(identity_lines))
|
|
480
|
-
user_soul = str(reg.get("user_soul") or "").strip()
|
|
496
|
+
user_soul = str(runtime_reg.get("user_soul") or reg.get("user_soul") or "").strip()
|
|
481
497
|
if user_soul:
|
|
482
498
|
blocks.append("## Additional instructions\n" + user_soul)
|
|
483
499
|
return "\n\n".join(blocks).strip()
|
|
@@ -735,6 +751,9 @@ def _agent_db_display_name(template_slug: str, workspace_id: str = "") -> str:
|
|
|
735
751
|
).fetchone()
|
|
736
752
|
if row and str(row["display_name"] or "").strip():
|
|
737
753
|
return str(row["display_name"]).strip()
|
|
754
|
+
except sqlite3.Error:
|
|
755
|
+
# Native profile seeding can precede schema creation on a clean install.
|
|
756
|
+
return ""
|
|
738
757
|
finally:
|
|
739
758
|
conn.close()
|
|
740
759
|
return ""
|
|
@@ -1105,6 +1124,7 @@ def profile_create(
|
|
|
1105
1124
|
results["steps"].extend(lane.get("steps") or [])
|
|
1106
1125
|
except Exception as exc:
|
|
1107
1126
|
results["steps"].append({"step": "bootstrap_dm_lane", "ok": False, "error": str(exc)})
|
|
1127
|
+
results["ok"] = False
|
|
1108
1128
|
return results
|
|
1109
1129
|
|
|
1110
1130
|
prof = _srv().safe_profile_slug(name)
|
|
@@ -1242,7 +1262,7 @@ def profile_create(
|
|
|
1242
1262
|
except Exception as exc:
|
|
1243
1263
|
results["steps"].append({"step": "bootstrap_dm_lane", "ok": False, "error": str(exc)})
|
|
1244
1264
|
|
|
1245
|
-
results["ok"] =
|
|
1265
|
+
results["ok"] = not any(step.get("ok") is False for step in results["steps"])
|
|
1246
1266
|
return results
|
|
1247
1267
|
|
|
1248
1268
|
|
|
@@ -66,6 +66,31 @@ def install_window_open(db_path: str) -> bool:
|
|
|
66
66
|
return not bool(stack_config.get_stack_config().get("install_complete"))
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def install_auth_email_allowed(email: str) -> tuple[bool, dict[str, Any]]:
|
|
70
|
+
"""During install, only the configured admin email may register until verified."""
|
|
71
|
+
normalized = str(email or "").strip().lower()
|
|
72
|
+
if not normalized or "@" not in normalized:
|
|
73
|
+
return False, {"error": "email required"}
|
|
74
|
+
if not install_window_open(""):
|
|
75
|
+
return True, {}
|
|
76
|
+
smtp = stack_config.get_stack_config().get("smtp") or {}
|
|
77
|
+
configured = str(smtp.get("admin_email") or "").strip().lower()
|
|
78
|
+
verified_admin = str(smtp.get("admin_email") or "").strip().lower() if stack_config.install_admin_verified() else ""
|
|
79
|
+
if verified_admin:
|
|
80
|
+
if normalized == verified_admin:
|
|
81
|
+
return True, {}
|
|
82
|
+
return False, {
|
|
83
|
+
"error": "install_admin_email_required",
|
|
84
|
+
"message": "Sign in with the admin email for this install.",
|
|
85
|
+
}
|
|
86
|
+
if configured and normalized != configured:
|
|
87
|
+
return False, {
|
|
88
|
+
"error": "install_admin_email_required",
|
|
89
|
+
"message": "Use the admin email entered at the start of setup.",
|
|
90
|
+
}
|
|
91
|
+
return True, {}
|
|
92
|
+
|
|
93
|
+
|
|
69
94
|
def install_owner_claimed(db_path: str) -> bool:
|
|
70
95
|
"""True once the first install admin has verified and owns the default workspace."""
|
|
71
96
|
try:
|
|
@@ -119,12 +144,19 @@ def _wizard_step_index(step: str) -> int:
|
|
|
119
144
|
|
|
120
145
|
def _derive_install_wizard_step(db_path: str) -> str:
|
|
121
146
|
cfg = stack_config.get_stack_config()
|
|
147
|
+
smtp = cfg.get("smtp") if isinstance(cfg.get("smtp"), dict) else {}
|
|
148
|
+
if not str(smtp.get("admin_email") or "").strip():
|
|
149
|
+
return "intro"
|
|
150
|
+
if not install_owner_claimed(db_path):
|
|
151
|
+
return "intro"
|
|
122
152
|
mode = str(cfg.get("deployment_mode") or "").strip()
|
|
123
153
|
if not mode:
|
|
124
154
|
return "welcome"
|
|
125
155
|
if mode == "public_multi_user" and not str(cfg.get("app_base_url") or "").strip():
|
|
126
156
|
return "publish"
|
|
127
|
-
if mode != "single_user_local" and not stack_config.install_admin_verified()
|
|
157
|
+
if mode != "single_user_local" and not stack_config.install_admin_verified():
|
|
158
|
+
if stack_config.smtp_setup_complete():
|
|
159
|
+
return "admin_auth"
|
|
128
160
|
return "smtp"
|
|
129
161
|
|
|
130
162
|
settings = _default_workspace_settings(db_path)
|
|
@@ -142,16 +174,18 @@ def resolve_install_wizard_step(db_path: str) -> str:
|
|
|
142
174
|
raw = stack_config.read_stack_raw()
|
|
143
175
|
saved = str(raw.get("wizard_step") or "").strip()
|
|
144
176
|
if saved in INSTALL_WIZARD_STEPS:
|
|
145
|
-
|
|
177
|
+
derived_idx = _wizard_step_index(derived)
|
|
178
|
+
saved_idx = _wizard_step_index(saved)
|
|
179
|
+
# Never resume ahead of incomplete gates (e.g. saved smtp before deployment).
|
|
180
|
+
if derived_idx >= 0 and saved_idx >= 0 and saved_idx != derived_idx:
|
|
146
181
|
return derived
|
|
147
182
|
return saved
|
|
148
183
|
return derived
|
|
149
184
|
|
|
150
185
|
|
|
151
186
|
def install_wizard_public_payload(db_path: str) -> dict[str, Any]:
|
|
152
|
-
admin_verified = stack_config.install_admin_verified() or install_owner_claimed(db_path)
|
|
153
187
|
return {
|
|
154
|
-
"admin_verified":
|
|
188
|
+
"admin_verified": stack_config.install_admin_verified(),
|
|
155
189
|
"resume_step": resolve_install_wizard_step(db_path),
|
|
156
190
|
"owner_claimed": install_owner_claimed(db_path),
|
|
157
191
|
}
|
|
@@ -76,6 +76,21 @@ def _binding_session_for(profile: str, source_id: str, client_id: str, binding_v
|
|
|
76
76
|
row = _binding_row_for(profile, source_id, client_id, binding_version)
|
|
77
77
|
return str((row or {}).get("session_id") or "").strip()
|
|
78
78
|
|
|
79
|
+
|
|
80
|
+
def _ensure_profile_api_for_session_create(
|
|
81
|
+
profile: str,
|
|
82
|
+
user_id: str,
|
|
83
|
+
workspace_id: str,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""WF-034: bind skips provider bootstrap; POST /api/sessions still needs a live gateway."""
|
|
86
|
+
return _srv().ensure_profile_api(
|
|
87
|
+
profile,
|
|
88
|
+
user_id,
|
|
89
|
+
workspace_id,
|
|
90
|
+
bootstrap_providers=False,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
79
94
|
def profile_chat_session(profile: str, payload: dict[str, Any], user_id: str = "") -> dict[str, Any]:
|
|
80
95
|
source_id = str(payload.get("source_id") or "ui").strip() or "ui"
|
|
81
96
|
client_id = str(payload.get("client_id") or "default").strip() or "default"
|
|
@@ -167,6 +182,13 @@ def profile_chat_session(profile: str, payload: dict[str, Any], user_id: str = "
|
|
|
167
182
|
sid = str(existing_row["session_id"]).strip()
|
|
168
183
|
if not sid:
|
|
169
184
|
existing_row = None
|
|
185
|
+
if existing_row and _srv()._is_space_room(str(room["room_type"]), room["agent_profile_id"]):
|
|
186
|
+
# Legacy room mentions could have created one session per
|
|
187
|
+
# user's private runtime. Shared rooms must converge on the
|
|
188
|
+
# template/native Hermes profile before reusing a row.
|
|
189
|
+
gateway_sid = str(existing_row["gateway_session_id"] or "").strip()
|
|
190
|
+
if not gateway_sid.startswith(f"api:{hermes_prof}:"):
|
|
191
|
+
existing_row = None
|
|
170
192
|
if existing_row:
|
|
171
193
|
sid = str(existing_row["session_id"]).strip()
|
|
172
194
|
gateway_sid = str(existing_row["gateway_session_id"] or f"api:{hermes_prof}:{sid}").strip()
|
|
@@ -211,6 +233,7 @@ def profile_chat_session(profile: str, payload: dict[str, Any], user_id: str = "
|
|
|
211
233
|
}
|
|
212
234
|
new_id = f"wf_room_{room_id[:8]}_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
|
213
235
|
requested_title = str(payload.get("title") or "").strip() or _srv()._default_session_title(template_prof)
|
|
236
|
+
lifecycle = _ensure_profile_api_for_session_create(hermes_prof, payer, workspace_id)
|
|
214
237
|
status, data, session_title = _srv()._create_profile_session_via_api(hermes_prof, new_id, requested_title)
|
|
215
238
|
if status >= 300:
|
|
216
239
|
raise ValueError(f"create session failed: {data}")
|
|
@@ -265,6 +288,7 @@ def profile_chat_session(profile: str, payload: dict[str, Any], user_id: str = "
|
|
|
265
288
|
}
|
|
266
289
|
new_id = f"wf_{source_id}_{client_id}_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
|
267
290
|
requested_title = str(payload.get("title") or "").strip() or _srv()._default_session_title(template_prof)
|
|
291
|
+
lifecycle = _ensure_profile_api_for_session_create(hermes_prof, payer, workspace_id)
|
|
268
292
|
status, data, session_title = _srv()._create_profile_session_via_api(hermes_prof, new_id, requested_title)
|
|
269
293
|
if status >= 300:
|
|
270
294
|
raise ValueError(f"create session failed: {data}")
|