create-workframe 0.1.14 → 0.1.16
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 +4 -0
- 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-esSBm9C_.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-j-DiBdGu.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-CaEDd3np.js → architectureDiagram-3BPJPVTR-DVVz9RGn.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DHJhMrIS.js → blockDiagram-GPEHLZMM-B6i10GMX.js} +1 -1
- package/workframe-ui/public/assets/brave-B885JYnH.svg +11 -0
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Cdyz9hIV.js → c4Diagram-AAUBKEIU-BqwJ7JoH.js} +1 -1
- package/workframe-ui/public/assets/channel-CJ9pFXbx.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-BbZx76LX.js → chunk-2J33WTMH-CyhcedSa.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DcSxjTIP.js → chunk-3OPIFGDE-1Rr6WuvI.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-B7F-kTY1.js → chunk-4BX2VUAB-D2bRrARV.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-BwwEiRTK.js → chunk-55IACEB6-CKXeZxSr.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DAi_NksX.js → chunk-5ZQYHXKU-DYQxxPZ4.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BIaF4XCN.js → chunk-727SXJPM-CGpIgqvD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DjYSk1dG.js → chunk-AQP2D5EJ-B88NGQ7d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-_twnyfgV.js → chunk-BSJP7CBP-CCymiI0O.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CkNmdy5s.js → chunk-CSCIHK7Q-DRgey03j.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CjLqd-gl.js → chunk-FMBD7UC4-l1adn8gM.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-_6oNqFjj.js → chunk-KSCS5N6A-CaEvDRPK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CeO9Am_D.js → chunk-L5ZTLDWV-BgCnvPTv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-sRM4YeIe.js → chunk-LZXEDZCA-t2zokHpn.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CH8tMb21.js → chunk-ND2GUHAM-C3c9p0sv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-DAI1bsII.js → chunk-NZK2D7GU-DSujF-R8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B2e-5a2G.js → chunk-O5CBEL6O-DJJTbM6p.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Bxk3enxo.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DdbkX1rg.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-DM2-0q37.js → chunk-XPW4576I-D2Cxn3AG.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-bwCRiiUl.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-bwCRiiUl.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-BnbbewLv.js → cose-bilkent-S5V4N54A---dQ-pFK.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-C4zmB1Vt.js → dagre-BM42HDAG-B_qe1ODY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-n-BJmsiC.js → diagram-2AECGRRQ-Bou8vEYl.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzQGQnZi.js → diagram-5GNKFQAL-DEUEzONU.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Cx_De-wS.js → diagram-KO2AKTUF-Ctq571VA.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CIOI3Lok.js → diagram-LMA3HP47-CnKn89lm.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-DkRuPLWY.js → diagram-OG6HWLK6-DnKlrMx2.js} +1 -1
- package/workframe-ui/public/assets/{dist-XhCT1Q-V.js → dist-ubrEgQyZ.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-C3GwhDU1.js → erDiagram-TEJ5UH35-PjqO7CJd.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-g-Xo_eS4.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CLWH8z2e.js → flowDiagram-I6XJVG4X-B2PN_nNG.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-D4h66Vy3.js → ganttDiagram-6RSMTGT7-ThyAtNQP.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-CB2UgoWy.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BjNIbmRM.js → gitGraphDiagram-PVQCEYII-DRJJ-zT-.js} +1 -1
- package/workframe-ui/public/assets/index-1euVfd3b.js +132 -0
- package/workframe-ui/public/assets/index-BLVAsAeO.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CcJspM79.js → info-J43DQDTF-CLtR33Q_.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-DP4xR9jt.js → infoDiagram-5YYISTIA-Cmunqfvi.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-D4cZb21V.js → ishikawaDiagram-YF4QCWOH-DWE_84eL.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DsLuHkIJ.js → journeyDiagram-JHISSGLW-BtBhpyRL.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Wu65nOsR.js → kanban-definition-UN3LZRKU-B8QspDDL.js} +1 -1
- package/workframe-ui/public/assets/{line-ByBDzRpy.js → line-iOgDn4O8.js} +1 -1
- package/workframe-ui/public/assets/{linear-DzXi2vnq.js → linear-BBehZNVA.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CaB-1Ckn.js → mermaid-parser.core-DQK5HcdU.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-D7HTHC17.js → mermaid.core-Dsrdd1Wr.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BfKsC9aj.js → mindmap-definition-RKZ34NQL-Ds_BfsJA.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-ClpU98TO.js → packet-YPE3B663-Bz3pi4i8.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-KeOyY_-u.js → pie-LRSECV5Y-BFFah-6g.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CUpjnzbs.js → pieDiagram-4H26LBE5-CVCvLPcC.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CJdPTszF.js → quadrantDiagram-W4KKPZXB-Cx6rrc_7.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C9BpandZ.js → radar-GUYGQ44K-DJ5vN_cg.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-CHDZPwry.js → requirementDiagram-4Y6WPE33-Cbry6b5c.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-CMJJh91s.js → sankeyDiagram-5OEKKPKP-CICUr5En.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BsV4GppP.js → sequenceDiagram-3UESZ5HK-BsOuB3F-.js} +1 -1
- package/workframe-ui/public/assets/{src-WHks82Up.js → src-TS8jZspq.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-eSX9P8z0.js → stateDiagram-AJRCARHV-avBzMQy_.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-BFRA5TYD.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-Dg1UkhGo.js → timeline-definition-PNZ67QCA-DHLm6UFN.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-B7yGsBuj.js → treeView-BLDUP644-B3Qd5t3j.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-j_G2oyQ_.js → treemap-LRROVOQU-D1dMQS7x.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BRrT0jqC.js → vennDiagram-CIIHVFJN-DZ6SoniY.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-DEdUqez2.js → wardley-L42UT6IY-9gK17Tp3.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-C-bBK9Bb.js → wardleyDiagram-YWT4CUSO-FCU_wmRE.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DhH5xuR2.js → xychartDiagram-2RQKCTM6-DWowvseN.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
package/README.md
CHANGED
package/bin/create-workframe.js
CHANGED
|
@@ -396,6 +396,8 @@ ${hermesServiceVolumesBlock(hermesHome)}
|
|
|
396
396
|
- ./Agents:/opt/data
|
|
397
397
|
- ./Files:${WORKSPACE_DATA_MOUNT}
|
|
398
398
|
- ./scripts:/opt/install/scripts:ro
|
|
399
|
+
- .:/project:ro
|
|
400
|
+
- .:/compose:ro
|
|
399
401
|
- workframe-proxy-token:/run/workframe-proxy
|
|
400
402
|
env_file:
|
|
401
403
|
- ./.env
|
|
@@ -528,6 +530,8 @@ services:
|
|
|
528
530
|
- \${WORKFRAME_HOST_PROJECT_ROOT}/Agents:/opt/data
|
|
529
531
|
- \${WORKFRAME_HOST_PROJECT_ROOT}/Files:${WORKSPACE_DATA_MOUNT}
|
|
530
532
|
- \${WORKFRAME_HOST_PROJECT_ROOT}/scripts:/opt/install/scripts:ro
|
|
533
|
+
- \${WORKFRAME_HOST_PROJECT_ROOT}:/project:ro
|
|
534
|
+
- \${WORKFRAME_HOST_COMPOSE_DIR}:/compose:ro
|
|
531
535
|
- workframe-proxy-token:/run/workframe-proxy
|
|
532
536
|
|
|
533
537
|
workframe-supervisor:
|
package/package.json
CHANGED
|
@@ -338,7 +338,7 @@ def _kanban_activity(
|
|
|
338
338
|
assignee = str(r["assignee"] or "kanban")
|
|
339
339
|
member = crew_lookup.get(assignee.lower()) or crew_lookup.get(_srv()._profile_slug(assignee).lower())
|
|
340
340
|
agent = _resolve_agent(member, _srv()._profile_display_name(assignee) if assignee != "kanban" else "Kanban")
|
|
341
|
-
desc = f"{r['title'] or 'task'}
|
|
341
|
+
desc = f"{r['title'] or 'task'} · {r['kind']}"
|
|
342
342
|
if len(desc) > 140:
|
|
343
343
|
desc = desc[:137] + "..."
|
|
344
344
|
st = str(r["status"] or r["kind"] or "event").lower()
|
|
@@ -638,9 +638,9 @@ def _room_session_activity_label(
|
|
|
638
638
|
) -> str:
|
|
639
639
|
base = str(title or room_name or "Session").strip()
|
|
640
640
|
if msg_count > 0:
|
|
641
|
-
return f"{base}
|
|
641
|
+
return f"{base} · {msg_count} message{'s' if msg_count != 1 else ''}"
|
|
642
642
|
if status == "active":
|
|
643
|
-
return f"{base}
|
|
643
|
+
return f"{base} · active"
|
|
644
644
|
return base
|
|
645
645
|
|
|
646
646
|
|
|
@@ -689,7 +689,7 @@ def room_activity_data(
|
|
|
689
689
|
ended = info.get("ended_at")
|
|
690
690
|
row_status = str(row["status"] or "active")
|
|
691
691
|
if row_status == "active" and not ended:
|
|
692
|
-
status = "
|
|
692
|
+
status = "idle"
|
|
693
693
|
elif ended:
|
|
694
694
|
status = "completed"
|
|
695
695
|
else:
|
|
@@ -721,7 +721,7 @@ def room_activity_data(
|
|
|
721
721
|
merged.extend(_message_activity_for_sessions(prof, crew_lookup, sids, ACTIVITY_ROOM_LIMIT))
|
|
722
722
|
|
|
723
723
|
try:
|
|
724
|
-
merged.extend(
|
|
724
|
+
merged.extend(_run_ledger_activity(room_id, crew_lookup, ACTIVITY_ROOM_LIMIT))
|
|
725
725
|
except Exception: # noqa: BLE001
|
|
726
726
|
pass
|
|
727
727
|
|
|
@@ -729,6 +729,54 @@ def room_activity_data(
|
|
|
729
729
|
return merged[:ACTIVITY_ROOM_LIMIT]
|
|
730
730
|
|
|
731
731
|
|
|
732
|
+
def _run_event_activity_status(event_type: str, run_status: str) -> str:
|
|
733
|
+
et = str(event_type or "").strip().lower()
|
|
734
|
+
if et == "run.completed":
|
|
735
|
+
return "completed"
|
|
736
|
+
if et in ("run.failed", "run.denied"):
|
|
737
|
+
return "failed"
|
|
738
|
+
rs = str(run_status or "").strip().lower()
|
|
739
|
+
if rs == "running":
|
|
740
|
+
return "active"
|
|
741
|
+
if rs == "completed":
|
|
742
|
+
return "completed"
|
|
743
|
+
if rs in ("failed", "denied", "cancelled"):
|
|
744
|
+
return "failed"
|
|
745
|
+
return "idle"
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _run_ledger_activity(
|
|
749
|
+
room_id: str,
|
|
750
|
+
crew_lookup: dict[str, dict[str, Any]],
|
|
751
|
+
limit: int,
|
|
752
|
+
) -> list[dict[str, Any]]:
|
|
753
|
+
"""User-facing run ledger rows — skip internal authorize noise, resolve agent names."""
|
|
754
|
+
rows = run_ledger.list_run_events_for_room(room_id, limit=limit)
|
|
755
|
+
out: list[dict[str, Any]] = []
|
|
756
|
+
for row in rows:
|
|
757
|
+
event_type = str(row.get("event_type") or "")
|
|
758
|
+
if event_type == "run.authorized":
|
|
759
|
+
continue
|
|
760
|
+
prof = str(row.get("profile") or "").strip()
|
|
761
|
+
member = crew_lookup.get(prof.lower()) or crew_lookup.get(_srv()._profile_slug(prof).lower())
|
|
762
|
+
agent = _resolve_agent(member, _srv()._profile_display_name(prof))
|
|
763
|
+
desc = str(row.get("task_description") or "").strip()
|
|
764
|
+
if not desc:
|
|
765
|
+
continue
|
|
766
|
+
out.append(
|
|
767
|
+
{
|
|
768
|
+
**row,
|
|
769
|
+
"agent_name": agent["agent_name"],
|
|
770
|
+
"key": agent["key"],
|
|
771
|
+
"profile": prof or agent["profile"],
|
|
772
|
+
"task_description": desc,
|
|
773
|
+
"status": _run_event_activity_status(event_type, str(row.get("run_status") or row.get("status") or "")),
|
|
774
|
+
"source": "run_ledger",
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
return out
|
|
778
|
+
|
|
779
|
+
|
|
732
780
|
def _room_agent_dm_owner_user_id(conn: sqlite3.Connection, room_id: str) -> str | None:
|
|
733
781
|
row = conn.execute(
|
|
734
782
|
"""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Public API error codes and sanitization — never leak tracebacks to clients."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_TRACE = re.compile(r"\n(?:Traceback| File )", re.MULTILINE)
|
|
8
|
+
_SNAKE_CODE = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
|
|
9
|
+
|
|
10
|
+
PUBLIC: dict[str, dict[str, str]] = {
|
|
11
|
+
"request_failed": {
|
|
12
|
+
"message": "Something went wrong.",
|
|
13
|
+
"hint": "Try again in a moment. If it keeps happening, contact your admin.",
|
|
14
|
+
},
|
|
15
|
+
"runtime_profile_permission_denied": {
|
|
16
|
+
"message": "Workframe could not prepare your agent workspace on disk.",
|
|
17
|
+
"hint": "An admin should restart the stack or fix permissions on the Agents data volume.",
|
|
18
|
+
},
|
|
19
|
+
"runtime_profile_create_failed": {
|
|
20
|
+
"message": "Workframe could not create your personal agent profile.",
|
|
21
|
+
"hint": "Try again in a moment. If it persists, restart Workframe or contact your admin.",
|
|
22
|
+
},
|
|
23
|
+
"runtime_profile_api_config_failed": {
|
|
24
|
+
"message": "Workframe could not start the agent API for this profile.",
|
|
25
|
+
"hint": "Check that the Hermes gateway is running and try again.",
|
|
26
|
+
},
|
|
27
|
+
"runtime_profile_bootstrap_failed": {
|
|
28
|
+
"message": "Workframe could not finish setting up this agent profile.",
|
|
29
|
+
"hint": "Try again or pick another agent from the crew list.",
|
|
30
|
+
},
|
|
31
|
+
"session_bootstrap_failed": {
|
|
32
|
+
"message": "Workframe could not start this chat session.",
|
|
33
|
+
"hint": "Refresh the page or open another agent room.",
|
|
34
|
+
},
|
|
35
|
+
"room_not_found": {
|
|
36
|
+
"message": "That chat room was not found.",
|
|
37
|
+
"hint": "Refresh the room list or open another chat.",
|
|
38
|
+
},
|
|
39
|
+
"room_access_denied": {
|
|
40
|
+
"message": "You cannot access this room.",
|
|
41
|
+
"hint": "Ask a workspace admin for access.",
|
|
42
|
+
},
|
|
43
|
+
"no_session": {
|
|
44
|
+
"message": "You are not signed in.",
|
|
45
|
+
"hint": "Refresh the page and sign in again.",
|
|
46
|
+
},
|
|
47
|
+
"model_required": {
|
|
48
|
+
"message": "Choose a model for this agent.",
|
|
49
|
+
"hint": "Open LLM models, select a connected provider and model, then create the agent.",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def scrub_internal(text: str) -> str:
|
|
55
|
+
"""Drop tracebacks and multiline internals — keep a single safe line for logs."""
|
|
56
|
+
cleaned = str(text or "").strip()
|
|
57
|
+
if not cleaned:
|
|
58
|
+
return ""
|
|
59
|
+
if _TRACE.search(cleaned):
|
|
60
|
+
cleaned = _TRACE.split(cleaned, maxsplit=1)[0].strip()
|
|
61
|
+
if "\n" in cleaned:
|
|
62
|
+
cleaned = cleaned.split("\n", 1)[0].strip()
|
|
63
|
+
return cleaned
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def classify_error(text: str) -> str | None:
|
|
67
|
+
raw_full = str(text or "")
|
|
68
|
+
lowered_full = raw_full.lower()
|
|
69
|
+
if "permissionerror" in lowered_full or "permission denied" in lowered_full:
|
|
70
|
+
return "runtime_profile_permission_denied"
|
|
71
|
+
raw = scrub_internal(text)
|
|
72
|
+
if not raw:
|
|
73
|
+
return None
|
|
74
|
+
lowered = raw.lower()
|
|
75
|
+
if "runtime profile create failed" in lowered:
|
|
76
|
+
return "runtime_profile_create_failed"
|
|
77
|
+
if "runtime profile api config failed" in lowered:
|
|
78
|
+
return "runtime_profile_api_config_failed"
|
|
79
|
+
if "runtime profile bootstrap failed" in lowered:
|
|
80
|
+
return "runtime_profile_bootstrap_failed"
|
|
81
|
+
if "connection refused" in lowered_full or "errno 111" in lowered_full:
|
|
82
|
+
return "runtime_profile_api_config_failed"
|
|
83
|
+
if "profile api did not become healthy" in lowered or "profile api unavailable" in lowered:
|
|
84
|
+
return "runtime_profile_api_config_failed"
|
|
85
|
+
if "session creation failed" in lowered or "create session failed" in lowered:
|
|
86
|
+
return "session_bootstrap_failed"
|
|
87
|
+
if "room_not_found" in lowered or "room not found" in lowered:
|
|
88
|
+
return "room_not_found"
|
|
89
|
+
if "room_access_denied" in lowered or "access denied" in lowered:
|
|
90
|
+
return "room_access_denied"
|
|
91
|
+
if "no_session" in lowered:
|
|
92
|
+
return "no_session"
|
|
93
|
+
if _SNAKE_CODE.fullmatch(raw):
|
|
94
|
+
return raw
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def public_api_error_payload(exc: BaseException, *, context: str = "") -> dict[str, Any]:
|
|
99
|
+
"""Map an exception to a client-safe {error, message, hint} payload."""
|
|
100
|
+
full = str(exc)
|
|
101
|
+
code = classify_error(full)
|
|
102
|
+
if code and code in PUBLIC:
|
|
103
|
+
row = PUBLIC[code]
|
|
104
|
+
return {"error": code, "message": row["message"], "hint": row.get("hint", "")}
|
|
105
|
+
if code and _SNAKE_CODE.fullmatch(code):
|
|
106
|
+
human = code.replace("_", " ").strip().capitalize() + "."
|
|
107
|
+
return {"error": code, "message": human, "hint": PUBLIC["request_failed"]["hint"]}
|
|
108
|
+
fallback = PUBLIC["request_failed"]
|
|
109
|
+
payload: dict[str, Any] = {
|
|
110
|
+
"error": "request_failed",
|
|
111
|
+
"message": fallback["message"],
|
|
112
|
+
"hint": fallback["hint"],
|
|
113
|
+
}
|
|
114
|
+
if context.strip():
|
|
115
|
+
payload["context"] = context.strip()
|
|
116
|
+
return payload
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def http_status_for_code(code: str) -> int:
|
|
120
|
+
if code in ("no_session",):
|
|
121
|
+
return 401
|
|
122
|
+
if code in ("room_access_denied", "forbidden"):
|
|
123
|
+
return 403
|
|
124
|
+
if code in ("room_not_found",):
|
|
125
|
+
return 404
|
|
126
|
+
return 400
|
|
@@ -27,6 +27,7 @@ def profile_chat_bind(profile: str, payload: dict[str, Any], user_id: str = "")
|
|
|
27
27
|
session = lane_bindings.profile_chat_session(profile, payload, user_id)
|
|
28
28
|
sid = str(session.get("session_id") or "").strip()
|
|
29
29
|
hermes_prof = str(session.get("profile") or _srv().resolve_validated_profile(profile))
|
|
30
|
+
workspace_id = str(session.get("workspace_id") or payload.get("workspace_id") or "").strip()
|
|
30
31
|
template_prof = str(session.get("template_profile") or "").strip()
|
|
31
32
|
if not template_prof:
|
|
32
33
|
template_prof = (
|
|
@@ -34,7 +35,13 @@ def profile_chat_bind(profile: str, payload: dict[str, Any], user_id: str = "")
|
|
|
34
35
|
if _srv()._is_runtime_profile_slug(hermes_prof)
|
|
35
36
|
else _srv().resolve_validated_profile(profile)
|
|
36
37
|
)
|
|
37
|
-
|
|
38
|
+
if user_id and workspace_id and session.get("llm_ready"):
|
|
39
|
+
llm_provider = _srv()._llm_billing_provider(
|
|
40
|
+
hermes_prof, user_id=user_id, workspace_id=workspace_id,
|
|
41
|
+
)
|
|
42
|
+
_srv()._schedule_profile_lease_yaml_reconcile(
|
|
43
|
+
hermes_prof, user_id, workspace_id, llm_provider,
|
|
44
|
+
)
|
|
38
45
|
history = _srv().chat_messages(hermes_prof, sid)
|
|
39
46
|
# ponytail: cohort manifest is lazy — GET /api/me/cohort; bind must stay fast
|
|
40
47
|
cohort: list[dict[str, Any]] = []
|
|
@@ -205,6 +205,34 @@ def _iter_profile_stream_frames(upstream: Any) -> Iterator[tuple[str, dict[str,
|
|
|
205
205
|
if parsed:
|
|
206
206
|
yield parsed
|
|
207
207
|
|
|
208
|
+
def _stream_data_text(data_lines: list[str]) -> str:
|
|
209
|
+
raw = "\n".join(line.strip() for line in data_lines if line.strip())
|
|
210
|
+
if not raw:
|
|
211
|
+
return ""
|
|
212
|
+
try:
|
|
213
|
+
data = json.loads(raw)
|
|
214
|
+
except json.JSONDecodeError:
|
|
215
|
+
return raw
|
|
216
|
+
if not isinstance(data, dict):
|
|
217
|
+
return raw
|
|
218
|
+
err = data.get("error")
|
|
219
|
+
if isinstance(err, dict):
|
|
220
|
+
nested = str(err.get("message") or err.get("detail") or "").strip()
|
|
221
|
+
if nested:
|
|
222
|
+
return nested
|
|
223
|
+
for key in ("message", "error", "text", "detail", "summary"):
|
|
224
|
+
val = data.get(key)
|
|
225
|
+
if isinstance(val, str) and val.strip():
|
|
226
|
+
return val.strip()
|
|
227
|
+
return raw
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _concierge_notice_from_error(text: str, *, provider: str) -> dict[str, Any]:
|
|
231
|
+
entry = llm_error_glossary.classify_exception_text(str(text or "").strip())
|
|
232
|
+
entry = llm_error_glossary.tailor_provider_entry(entry, provider)
|
|
233
|
+
return concierge.respond("", last_error=entry)
|
|
234
|
+
|
|
235
|
+
|
|
208
236
|
def _emit_stream_chat_error(
|
|
209
237
|
handler: BaseHTTPRequestHandler,
|
|
210
238
|
message: str = "",
|
|
@@ -276,18 +304,13 @@ def _emit_stream_error_body(
|
|
|
276
304
|
|
|
277
305
|
|
|
278
306
|
def _emit_stream_concierge(handler: BaseHTTPRequestHandler, entry: dict[str, Any]) -> None:
|
|
279
|
-
"""Deterministic assistant reply when LLM path is unavailable."""
|
|
307
|
+
"""Deterministic assistant reply when LLM path is unavailable (SSE headers already open)."""
|
|
280
308
|
payload = llm_error_glossary.notice_payload(entry)
|
|
281
309
|
text = str(payload.get("message") or "")
|
|
282
310
|
hint = str(payload.get("hint") or "").strip()
|
|
283
311
|
if hint:
|
|
284
312
|
text = f"{text}\n\n{hint}"
|
|
285
313
|
try:
|
|
286
|
-
handler.send_response(200)
|
|
287
|
-
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
288
|
-
handler.send_header("Cache-Control", "no-cache")
|
|
289
|
-
handler.send_header("Connection", "keep-alive")
|
|
290
|
-
handler.end_headers()
|
|
291
314
|
handler.wfile.write(
|
|
292
315
|
f"event: concierge\ndata: {json.dumps(payload)}\n\n".encode("utf-8"),
|
|
293
316
|
)
|
|
@@ -373,6 +396,14 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
373
396
|
prof, user_id=_triggering_user, workspace_id=_workspace_id, block=model_block,
|
|
374
397
|
)
|
|
375
398
|
model_used = str(model_block.get("default") or "").strip()
|
|
399
|
+
api_port = _srv()._profile_api_port(prof)
|
|
400
|
+
_open_profile_stream(
|
|
401
|
+
handler,
|
|
402
|
+
prof,
|
|
403
|
+
model_used=model_used,
|
|
404
|
+
llm_provider=llm_provider,
|
|
405
|
+
api_port=api_port,
|
|
406
|
+
)
|
|
376
407
|
_auth_decision: run_authority.RunAuthorityDecision | None = None
|
|
377
408
|
if _triggering_user:
|
|
378
409
|
run_ledger.ensure_schema()
|
|
@@ -427,14 +458,6 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
427
458
|
_log_llm_failure(handler, str(entry.get("code") or "no_llm_provider"), provider=llm_provider, profile=prof)
|
|
428
459
|
_emit_stream_concierge(handler, entry)
|
|
429
460
|
return
|
|
430
|
-
api_port = _srv()._profile_api_port(prof)
|
|
431
|
-
_open_profile_stream(
|
|
432
|
-
handler,
|
|
433
|
-
prof,
|
|
434
|
-
model_used=model_used,
|
|
435
|
-
llm_provider=llm_provider,
|
|
436
|
-
api_port=api_port,
|
|
437
|
-
)
|
|
438
461
|
try:
|
|
439
462
|
_srv().ensure_profile_api(
|
|
440
463
|
prof,
|
|
@@ -455,6 +478,7 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
455
478
|
_emit_stream_error_body(handler, entry=entry)
|
|
456
479
|
return
|
|
457
480
|
entry = llm_error_glossary.classify_exception_text(err_text)
|
|
481
|
+
entry = llm_error_glossary.tailor_provider_entry(entry, llm_provider)
|
|
458
482
|
_log_llm_failure(handler, str(entry.get("code") or ""), provider=llm_provider, profile=prof)
|
|
459
483
|
_emit_stream_error_body(handler, entry=entry)
|
|
460
484
|
return
|
|
@@ -484,6 +508,7 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
484
508
|
req = urllib.request.Request(url, data=upstream_body, headers=headers, method="POST")
|
|
485
509
|
saw_complete = False
|
|
486
510
|
complete_text = ""
|
|
511
|
+
last_classified_error: dict[str, Any] | None = None
|
|
487
512
|
try:
|
|
488
513
|
upstream = urllib.request.urlopen(req, timeout=3600)
|
|
489
514
|
except urllib.error.HTTPError as exc:
|
|
@@ -492,11 +517,16 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
492
517
|
data = json.loads(raw)
|
|
493
518
|
except Exception: # noqa: BLE001
|
|
494
519
|
data = {"error": raw or f"upstream stream failed: {exc.code}"}
|
|
495
|
-
|
|
520
|
+
if not isinstance(data, dict):
|
|
521
|
+
data = {"error": raw or f"upstream stream failed: {exc.code}"}
|
|
522
|
+
err_text = llm_error_glossary._extract_error_message(data, raw)
|
|
523
|
+
if not err_text:
|
|
524
|
+
err_text = raw or f"HTTP {exc.code}"
|
|
525
|
+
entry = _concierge_notice_from_error(err_text, provider=llm_provider)
|
|
496
526
|
_emit_stream_error_body(handler, entry=entry)
|
|
497
527
|
return
|
|
498
528
|
except OSError as exc:
|
|
499
|
-
entry =
|
|
529
|
+
entry = _concierge_notice_from_error(str(exc), provider=llm_provider)
|
|
500
530
|
_emit_stream_error_body(handler, entry=entry)
|
|
501
531
|
return
|
|
502
532
|
|
|
@@ -504,7 +534,7 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
504
534
|
buffer = b""
|
|
505
535
|
|
|
506
536
|
def _rewrite_event_frame(frame: bytes) -> bytes:
|
|
507
|
-
nonlocal saw_complete, complete_text
|
|
537
|
+
nonlocal saw_complete, complete_text, last_classified_error
|
|
508
538
|
text = frame.decode("utf-8", errors="replace")
|
|
509
539
|
lines = text.splitlines()
|
|
510
540
|
event_name = ""
|
|
@@ -539,6 +569,13 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
539
569
|
elif event_name == "error":
|
|
540
570
|
normalized = "error"
|
|
541
571
|
|
|
572
|
+
if normalized == "error" and data_lines:
|
|
573
|
+
err_text = _stream_data_text(data_lines)
|
|
574
|
+
if err_text:
|
|
575
|
+
notice = _concierge_notice_from_error(err_text, provider=llm_provider)
|
|
576
|
+
last_classified_error = notice
|
|
577
|
+
data_lines = [json.dumps(notice, separators=(",", ":"))]
|
|
578
|
+
|
|
542
579
|
output_lines: list[str] = []
|
|
543
580
|
if normalized:
|
|
544
581
|
output_lines.append(f"event: {normalized}")
|
|
@@ -575,22 +612,29 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
575
612
|
handler.wfile.write(rewritten)
|
|
576
613
|
handler.wfile.flush()
|
|
577
614
|
if not saw_complete or not complete_text:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if config_key.startswith(turn_credentials.LEASE_PREFIX) and not turn_credentials.validate_lease(
|
|
581
|
-
config_key,
|
|
582
|
-
):
|
|
583
|
-
entry = llm_error_glossary.playbook_entry("invalid_lease")
|
|
615
|
+
if last_classified_error:
|
|
616
|
+
entry = last_classified_error
|
|
584
617
|
else:
|
|
585
|
-
|
|
618
|
+
config_key = _srv()._read_config_model_api_key(prof)
|
|
619
|
+
_, model_name = _srv()._read_model_from_config(prof)
|
|
620
|
+
if config_key.startswith(turn_credentials.LEASE_PREFIX) and not turn_credentials.validate_lease(
|
|
621
|
+
config_key,
|
|
622
|
+
):
|
|
623
|
+
entry = llm_error_glossary.notice_payload(
|
|
624
|
+
llm_error_glossary.playbook_entry("invalid_lease"),
|
|
625
|
+
)
|
|
626
|
+
else:
|
|
627
|
+
entry = llm_error_glossary.notice_payload(
|
|
628
|
+
llm_error_glossary.playbook_entry("provider_empty_reply"),
|
|
629
|
+
)
|
|
586
630
|
_log_llm_failure(
|
|
587
631
|
handler,
|
|
588
632
|
str(entry.get("code") or "provider_empty_reply"),
|
|
589
633
|
provider=llm_provider,
|
|
590
|
-
model=str(model_name
|
|
634
|
+
model=str(model_name if not last_classified_error else ""),
|
|
591
635
|
profile=prof,
|
|
592
636
|
)
|
|
593
|
-
err_payload = json.dumps(
|
|
637
|
+
err_payload = json.dumps(entry)
|
|
594
638
|
handler.wfile.write(f"event: error\ndata: {err_payload}\n\n".encode("utf-8"))
|
|
595
639
|
handler.wfile.flush()
|
|
596
640
|
handler.wfile.write(b"event: done\ndata: {}\n\n")
|
|
@@ -96,12 +96,9 @@ def _workspace_agent_identities(workspace_id: str | None = None) -> dict[str, di
|
|
|
96
96
|
def _agent_identity_fields(profile: str, workspace_id: str | None = None, user_id: str = "") -> dict[str, Any]:
|
|
97
97
|
slug = _srv().safe_profile_slug(str(profile or "").strip())
|
|
98
98
|
template = _srv()._runtime_template_slug(slug) if _srv()._is_runtime_profile_slug(slug) else slug
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
reg_user = _agent_registry_row(runtime)
|
|
103
|
-
if reg_user.get("display_name") or reg_user.get("tagline") or reg_user.get("avatar_url"):
|
|
104
|
-
reg = {**reg, **{k: v for k, v in reg_user.items() if v}}
|
|
99
|
+
# Runtime profiles are credential/session proxies, not alternate agent
|
|
100
|
+
# identities. Shared template metadata is authoritative for all users.
|
|
101
|
+
reg = _agent_registry_row(template)
|
|
105
102
|
ident = _workspace_agent_identities(workspace_id).get(template, {})
|
|
106
103
|
reg_template = _agent_registry_row(template)
|
|
107
104
|
avatar_url: Any = None
|
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
6
|
import secrets
|
|
8
7
|
import urllib.parse
|
|
9
8
|
import urllib.request
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
11
|
+
import oauth_pending
|
|
12
12
|
import oidc_jwt
|
|
13
13
|
import stack_config
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
_PENDING: dict[str, dict[str, str]] = {}
|
|
14
|
+
from email_sender import APP_BASE_URL
|
|
15
|
+
GOOGLE_SIGNIN_PROVIDER = "google_signin"
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
def _google_creds() -> tuple[str, str]:
|
|
@@ -28,10 +27,13 @@ def _google_creds() -> tuple[str, str]:
|
|
|
28
27
|
def start_google_auth(invite_email: str = "", invite_token: str = "") -> dict[str, Any]:
|
|
29
28
|
client_id, _ = _google_creds()
|
|
30
29
|
state = secrets.token_urlsafe(24)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
meta = json.dumps(
|
|
31
|
+
{
|
|
32
|
+
"invite_email": invite_email.strip().lower(),
|
|
33
|
+
"invite_token": invite_token.strip(),
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
oauth_pending.store_pending(state, "", GOOGLE_SIGNIN_PROVIDER, "", meta)
|
|
35
37
|
redirect_uri = f"{APP_BASE_URL}/api/auth/google/callback"
|
|
36
38
|
params = {
|
|
37
39
|
"client_id": client_id,
|
|
@@ -49,9 +51,15 @@ def start_google_auth(invite_email: str = "", invite_token: str = "") -> dict[st
|
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
def exchange_google_code(code: str, state: str) -> dict[str, Any]:
|
|
52
|
-
pending =
|
|
53
|
-
if not pending:
|
|
54
|
+
pending = oauth_pending.take_pending(state)
|
|
55
|
+
if not pending or str(pending.get("provider") or "") != GOOGLE_SIGNIN_PROVIDER:
|
|
54
56
|
raise ValueError("invalid or expired OAuth state")
|
|
57
|
+
try:
|
|
58
|
+
meta = json.loads(str(pending.get("workspace_id") or "{}"))
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
meta = {}
|
|
61
|
+
if not isinstance(meta, dict):
|
|
62
|
+
meta = {}
|
|
55
63
|
client_id, client_secret = _google_creds()
|
|
56
64
|
redirect_uri = f"{APP_BASE_URL}/api/auth/google/callback"
|
|
57
65
|
data = urllib.parse.urlencode(
|
|
@@ -80,11 +88,11 @@ def exchange_google_code(code: str, state: str) -> dict[str, Any]:
|
|
|
80
88
|
raise ValueError("Google account has no email")
|
|
81
89
|
if claims.get("email_verified") is False:
|
|
82
90
|
raise ValueError("Google email is not verified")
|
|
83
|
-
invite_email =
|
|
91
|
+
invite_email = str(meta.get("invite_email") or "").strip().lower()
|
|
84
92
|
if invite_email and email != invite_email:
|
|
85
93
|
raise ValueError("Google email does not match the invited address")
|
|
86
94
|
return {
|
|
87
95
|
"email": email,
|
|
88
96
|
"display_name": str(claims.get("name") or email),
|
|
89
|
-
"invite_token":
|
|
97
|
+
"invite_token": str(meta.get("invite_token") or ""),
|
|
90
98
|
}
|