create-workframe 0.1.11 → 0.1.13
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 +85 -0
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +9 -0
- package/scripts/sync-canonical-to-package.mjs +9 -0
- package/scripts/verify-public-deploy.sh +121 -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/openrouter_catalog.py +4 -4
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_api_lifecycle.py +66 -0
- package/workframe-api/profile_config_yaml.py +126 -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 +470 -0
- package/workframe-api/run-typecheck.mjs +26 -0
- 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 +1376 -19305
- 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 +117 -0
- package/workframe-api/test_oauth_pending.py +41 -0
- package/workframe-api/test_profile_model_yaml.py +99 -0
- package/workframe-api/test_provider_bindings.py +52 -0
- package/workframe-api/test_provider_catalog.py +28 -0
- package/workframe-api/test_public_routes.py +41 -0
- package/workframe-api/test_route_registry.py +245 -0
- 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/time-bind-steps.py +75 -0
- package/workframe-api/time-perf.py +81 -0
- package/workframe-api/time-warm.py +107 -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-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
- package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
- package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
- package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
- package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
- package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
- package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
- package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
- package/workframe-ui/public/index.html +7 -6
- package/workframe-ui/public/workframe-build.json +5 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
- package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
- package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
- package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""WF-032 extract: OAuth PKCE pending state (short-lived callback handshake)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import secrets
|
|
8
|
+
import sqlite3
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import db_schema
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _srv():
|
|
16
|
+
import server as srv
|
|
17
|
+
|
|
18
|
+
return srv
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ensure_schema() -> None:
|
|
22
|
+
db_schema.ensure_oauth_pending_migrations(_srv()._workframe_db)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pkce_pair() -> tuple[str, str]:
|
|
26
|
+
verifier = secrets.token_urlsafe(48)
|
|
27
|
+
digest = hashlib.sha256(verifier.encode("utf-8")).digest()
|
|
28
|
+
challenge = base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=")
|
|
29
|
+
return verifier, challenge
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def store_pending(
|
|
33
|
+
state: str,
|
|
34
|
+
user_id: str,
|
|
35
|
+
provider: str,
|
|
36
|
+
code_verifier: str,
|
|
37
|
+
workspace_id: str = "",
|
|
38
|
+
) -> None:
|
|
39
|
+
ensure_schema()
|
|
40
|
+
now = _srv()._utc_now()
|
|
41
|
+
expires = (datetime.now(timezone.utc) + timedelta(minutes=15)).isoformat()
|
|
42
|
+
conn = _srv()._workframe_db()
|
|
43
|
+
try:
|
|
44
|
+
conn.execute(
|
|
45
|
+
"""
|
|
46
|
+
INSERT INTO oauth_pending_states (state, user_id, provider, code_verifier, workspace_id, expires_at, created_at)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
48
|
+
ON CONFLICT(state) DO UPDATE SET
|
|
49
|
+
user_id = excluded.user_id,
|
|
50
|
+
provider = excluded.provider,
|
|
51
|
+
code_verifier = excluded.code_verifier,
|
|
52
|
+
workspace_id = excluded.workspace_id,
|
|
53
|
+
expires_at = excluded.expires_at,
|
|
54
|
+
created_at = excluded.created_at
|
|
55
|
+
""",
|
|
56
|
+
(state, user_id, provider, code_verifier, workspace_id or None, expires, now),
|
|
57
|
+
)
|
|
58
|
+
conn.commit()
|
|
59
|
+
finally:
|
|
60
|
+
conn.close()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def take_pending(state: str) -> dict[str, Any] | None:
|
|
64
|
+
ensure_schema()
|
|
65
|
+
conn = _srv()._workframe_db()
|
|
66
|
+
conn.row_factory = sqlite3.Row
|
|
67
|
+
try:
|
|
68
|
+
row = conn.execute(
|
|
69
|
+
"SELECT * FROM oauth_pending_states WHERE state = ?",
|
|
70
|
+
(str(state or "").strip(),),
|
|
71
|
+
).fetchone()
|
|
72
|
+
if not row:
|
|
73
|
+
return None
|
|
74
|
+
conn.execute("DELETE FROM oauth_pending_states WHERE state = ?", (row["state"],))
|
|
75
|
+
conn.commit()
|
|
76
|
+
expires_at = str(row["expires_at"] or "")
|
|
77
|
+
if expires_at:
|
|
78
|
+
try:
|
|
79
|
+
if datetime.fromisoformat(expires_at.replace("Z", "+00:00")) < datetime.now(timezone.utc):
|
|
80
|
+
return None
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass
|
|
83
|
+
return dict(row)
|
|
84
|
+
finally:
|
|
85
|
+
conn.close()
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""WF-032 extract: GitHub, Stripe, and Discord OAuth redirect flows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import sqlite3
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import oauth_pending
|
|
16
|
+
import platform_auth
|
|
17
|
+
import provider_catalog
|
|
18
|
+
import stack_config
|
|
19
|
+
from email_sender import APP_BASE_URL
|
|
20
|
+
|
|
21
|
+
GITHUB_OAUTH_SCOPES = "repo read:user"
|
|
22
|
+
STRIPE_CONNECT_SCOPES = "read_write"
|
|
23
|
+
|
|
24
|
+
_store_oauth_pending = oauth_pending.store_pending
|
|
25
|
+
_take_oauth_pending = oauth_pending.take_pending
|
|
26
|
+
_pkce_pair = oauth_pending.pkce_pair
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _srv():
|
|
30
|
+
import server as srv
|
|
31
|
+
|
|
32
|
+
return srv
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _stripe_connect_app_config() -> dict[str, str]:
|
|
36
|
+
"""Stack Stripe Connect app — platform secret never leaves the BFF."""
|
|
37
|
+
stack_st = stack_config.resolved_stripe_connect()
|
|
38
|
+
if stack_st.get("client_id") and stack_st.get("client_secret"):
|
|
39
|
+
return stack_st
|
|
40
|
+
client_id = (
|
|
41
|
+
os.environ.get("WORKFRAME_STRIPE_CONNECT_CLIENT_ID")
|
|
42
|
+
or os.environ.get("STRIPE_CONNECT_CLIENT_ID")
|
|
43
|
+
or ""
|
|
44
|
+
).strip()
|
|
45
|
+
client_secret = (
|
|
46
|
+
os.environ.get("WORKFRAME_STRIPE_SECRET_KEY")
|
|
47
|
+
or os.environ.get("STRIPE_SECRET_KEY")
|
|
48
|
+
or ""
|
|
49
|
+
).strip()
|
|
50
|
+
return {"client_id": client_id, "client_secret": client_secret}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _stripe_connect_configured() -> bool:
|
|
54
|
+
cfg = _stripe_connect_app_config()
|
|
55
|
+
return bool(cfg.get("client_id") and cfg.get("client_secret"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _stripe_oauth_redirect_uri() -> str:
|
|
59
|
+
base = APP_BASE_URL.rstrip("/")
|
|
60
|
+
return f"{base}/api/oauth/stripe/callback"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_workspace_settings(row: sqlite3.Row | dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
keys = row.keys() if hasattr(row, "keys") else row
|
|
65
|
+
raw = ""
|
|
66
|
+
if isinstance(keys, (list, set)) or hasattr(keys, "__contains__"):
|
|
67
|
+
if "settings_json" in keys:
|
|
68
|
+
raw = row["settings_json"] if not isinstance(row, dict) else row.get("settings_json", "")
|
|
69
|
+
if not raw:
|
|
70
|
+
return {}
|
|
71
|
+
try:
|
|
72
|
+
parsed = json.loads(str(raw or "{}"))
|
|
73
|
+
except (TypeError, json.JSONDecodeError):
|
|
74
|
+
return {}
|
|
75
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _github_oauth_app_config(workspace_id: str = "") -> dict[str, str]:
|
|
79
|
+
"""Stack or workspace GitHub OAuth app — client secret never leaves the BFF."""
|
|
80
|
+
workspace = str(workspace_id or "").strip()
|
|
81
|
+
if workspace:
|
|
82
|
+
try:
|
|
83
|
+
conn = _srv()._workframe_db()
|
|
84
|
+
row = conn.execute(
|
|
85
|
+
"SELECT settings_json FROM workspaces WHERE id = ? AND deleted_at IS NULL",
|
|
86
|
+
(workspace,),
|
|
87
|
+
).fetchone()
|
|
88
|
+
conn.close()
|
|
89
|
+
if row:
|
|
90
|
+
gh = _parse_workspace_settings(row).get("github_oauth")
|
|
91
|
+
if isinstance(gh, dict):
|
|
92
|
+
client_id = str(gh.get("client_id") or "").strip()
|
|
93
|
+
client_secret = str(gh.get("client_secret") or "").strip()
|
|
94
|
+
if client_id and client_secret:
|
|
95
|
+
return {"client_id": client_id, "client_secret": client_secret}
|
|
96
|
+
except sqlite3.Error:
|
|
97
|
+
pass
|
|
98
|
+
stack_gh = stack_config.resolved_github_oauth()
|
|
99
|
+
if stack_gh.get("client_id") and stack_gh.get("client_secret"):
|
|
100
|
+
return stack_gh
|
|
101
|
+
client_id = (
|
|
102
|
+
os.environ.get("WORKFRAME_GITHUB_OAUTH_CLIENT_ID")
|
|
103
|
+
or os.environ.get("GITHUB_CLIENT_ID")
|
|
104
|
+
or ""
|
|
105
|
+
).strip()
|
|
106
|
+
client_secret = (
|
|
107
|
+
os.environ.get("WORKFRAME_GITHUB_OAUTH_CLIENT_SECRET")
|
|
108
|
+
or os.environ.get("GITHUB_CLIENT_SECRET")
|
|
109
|
+
or ""
|
|
110
|
+
).strip()
|
|
111
|
+
return {"client_id": client_id, "client_secret": client_secret}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _github_oauth_configured(workspace_id: str = "") -> bool:
|
|
115
|
+
cfg = _github_oauth_app_config(workspace_id)
|
|
116
|
+
return bool(cfg.get("client_id") and cfg.get("client_secret"))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _github_oauth_redirect_uri() -> str:
|
|
120
|
+
base = APP_BASE_URL.rstrip("/")
|
|
121
|
+
return f"{base}/api/oauth/github/callback"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _github_exchange_code(code: str, client_id: str, client_secret: str, redirect_uri: str, code_verifier: str) -> dict[str, Any]:
|
|
126
|
+
body = urllib.parse.urlencode(
|
|
127
|
+
{
|
|
128
|
+
"client_id": client_id,
|
|
129
|
+
"client_secret": client_secret,
|
|
130
|
+
"code": code,
|
|
131
|
+
"redirect_uri": redirect_uri,
|
|
132
|
+
"code_verifier": code_verifier,
|
|
133
|
+
},
|
|
134
|
+
).encode("utf-8")
|
|
135
|
+
req = urllib.request.Request(
|
|
136
|
+
"https://github.com/login/oauth/access_token",
|
|
137
|
+
data=body,
|
|
138
|
+
headers={
|
|
139
|
+
"Accept": "application/json",
|
|
140
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
141
|
+
},
|
|
142
|
+
method="POST",
|
|
143
|
+
)
|
|
144
|
+
try:
|
|
145
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
146
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
147
|
+
except urllib.error.HTTPError as exc:
|
|
148
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
149
|
+
except OSError as exc:
|
|
150
|
+
return {"error": str(exc)}
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(raw)
|
|
153
|
+
except json.JSONDecodeError:
|
|
154
|
+
return {"error": raw or "github_token_exchange_failed"}
|
|
155
|
+
return data if isinstance(data, dict) else {"error": "invalid_github_response"}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _start_github_oauth(user_id: str, workspace_id: str, spec: dict[str, Any]) -> dict[str, Any]:
|
|
159
|
+
cfg = _github_oauth_app_config(workspace_id)
|
|
160
|
+
client_id = str(cfg.get("client_id") or "").strip()
|
|
161
|
+
if not client_id or not str(cfg.get("client_secret") or "").strip():
|
|
162
|
+
return {
|
|
163
|
+
"ok": False,
|
|
164
|
+
"error": "github_oauth_not_configured",
|
|
165
|
+
"message": "Workframe admin must register a GitHub OAuth app under Workframe → Integrations.",
|
|
166
|
+
}
|
|
167
|
+
state = secrets.token_urlsafe(24)
|
|
168
|
+
verifier, challenge = _pkce_pair()
|
|
169
|
+
_store_oauth_pending(state, user_id, "github", verifier, workspace_id)
|
|
170
|
+
params = urllib.parse.urlencode(
|
|
171
|
+
{
|
|
172
|
+
"client_id": client_id,
|
|
173
|
+
"redirect_uri": _github_oauth_redirect_uri(),
|
|
174
|
+
"scope": GITHUB_OAUTH_SCOPES,
|
|
175
|
+
"state": state,
|
|
176
|
+
"code_challenge": challenge,
|
|
177
|
+
"code_challenge_method": "S256",
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
return {
|
|
181
|
+
"ok": True,
|
|
182
|
+
"provider": str(spec.get("id") or "github"),
|
|
183
|
+
"redirect_url": f"https://github.com/login/oauth/authorize?{params}",
|
|
184
|
+
"output": "",
|
|
185
|
+
"error": None,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _complete_github_oauth(user_id: str, code: str, state: str) -> dict[str, Any]:
|
|
190
|
+
pending = _take_oauth_pending(state)
|
|
191
|
+
if not pending or str(pending.get("user_id") or "") != user_id:
|
|
192
|
+
return {"ok": False, "error": "invalid_oauth_state"}
|
|
193
|
+
workspace_id = str(pending.get("workspace_id") or "")
|
|
194
|
+
cfg = _github_oauth_app_config(workspace_id)
|
|
195
|
+
client_id = str(cfg.get("client_id") or "").strip()
|
|
196
|
+
client_secret = str(cfg.get("client_secret") or "").strip()
|
|
197
|
+
if not client_id or not client_secret:
|
|
198
|
+
return {"ok": False, "error": "github_oauth_not_configured"}
|
|
199
|
+
token_data = _github_exchange_code(
|
|
200
|
+
code,
|
|
201
|
+
client_id,
|
|
202
|
+
client_secret,
|
|
203
|
+
_github_oauth_redirect_uri(),
|
|
204
|
+
str(pending.get("code_verifier") or ""),
|
|
205
|
+
)
|
|
206
|
+
if token_data.get("error"):
|
|
207
|
+
return {"ok": False, "error": str(token_data.get("error"))}
|
|
208
|
+
access_token = str(token_data.get("access_token") or "").strip()
|
|
209
|
+
if not access_token:
|
|
210
|
+
return {"ok": False, "error": "github_missing_access_token"}
|
|
211
|
+
spec = provider_catalog.catalog_provider("github") or {}
|
|
212
|
+
env_var = str(spec.get("env_var") or "GITHUB_TOKEN")
|
|
213
|
+
payload = _srv()._store_user_credential(user_id, "github", "oauth", access_token, env_var, "GitHub OAuth")
|
|
214
|
+
cred_ref = str(payload["credential_ref"])
|
|
215
|
+
now = _srv()._utc_now()
|
|
216
|
+
cred_id = str(uuid.uuid4())
|
|
217
|
+
try:
|
|
218
|
+
conn = sqlite3.connect(str(_srv()._workframe_db_path()), timeout=3.0)
|
|
219
|
+
conn.execute(
|
|
220
|
+
"""
|
|
221
|
+
INSERT INTO credential_bindings
|
|
222
|
+
(id, workspace_id, user_id, agent_profile_id, provider, credential_type,
|
|
223
|
+
credential_ref, label, is_active, created_by, created_at, updated_at)
|
|
224
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
|
225
|
+
""",
|
|
226
|
+
(
|
|
227
|
+
cred_id, None, user_id, None, "github", "oauth", cred_ref,
|
|
228
|
+
"GitHub OAuth", 1, user_id, now, now,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
conn.commit()
|
|
232
|
+
conn.close()
|
|
233
|
+
except sqlite3.Error as exc:
|
|
234
|
+
return {"ok": False, "error": f"db_error: {exc}"}
|
|
235
|
+
return {"ok": True, "provider": "github", "credential_id": cred_id}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _stripe_exchange_code(code: str, client_secret: str) -> dict[str, Any]:
|
|
239
|
+
body = urllib.parse.urlencode(
|
|
240
|
+
{
|
|
241
|
+
"client_secret": client_secret,
|
|
242
|
+
"code": code,
|
|
243
|
+
"grant_type": "authorization_code",
|
|
244
|
+
},
|
|
245
|
+
).encode("utf-8")
|
|
246
|
+
req = urllib.request.Request(
|
|
247
|
+
"https://connect.stripe.com/oauth/token",
|
|
248
|
+
data=body,
|
|
249
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
250
|
+
method="POST",
|
|
251
|
+
)
|
|
252
|
+
try:
|
|
253
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
254
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
255
|
+
except urllib.error.HTTPError as exc:
|
|
256
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
257
|
+
except OSError as exc:
|
|
258
|
+
return {"error": str(exc)}
|
|
259
|
+
try:
|
|
260
|
+
data = json.loads(raw)
|
|
261
|
+
except json.JSONDecodeError:
|
|
262
|
+
return {"error": raw or "stripe_token_exchange_failed"}
|
|
263
|
+
return data if isinstance(data, dict) else {"error": "invalid_stripe_response"}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _start_stripe_oauth(user_id: str, workspace_id: str, spec: dict[str, Any]) -> dict[str, Any]:
|
|
267
|
+
cfg = _stripe_connect_app_config()
|
|
268
|
+
client_id = str(cfg.get("client_id") or "").strip()
|
|
269
|
+
client_secret = str(cfg.get("client_secret") or "").strip()
|
|
270
|
+
if not client_id or not client_secret:
|
|
271
|
+
return {
|
|
272
|
+
"ok": False,
|
|
273
|
+
"error": "stripe_connect_not_configured",
|
|
274
|
+
"message": "Workframe admin must register Stripe Connect under Workframe → Integrations.",
|
|
275
|
+
}
|
|
276
|
+
state = secrets.token_urlsafe(24)
|
|
277
|
+
_store_oauth_pending(state, user_id, "stripe", "", workspace_id)
|
|
278
|
+
params = urllib.parse.urlencode(
|
|
279
|
+
{
|
|
280
|
+
"response_type": "code",
|
|
281
|
+
"client_id": client_id,
|
|
282
|
+
"scope": STRIPE_CONNECT_SCOPES,
|
|
283
|
+
"redirect_uri": _stripe_oauth_redirect_uri(),
|
|
284
|
+
"state": state,
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
return {
|
|
288
|
+
"ok": True,
|
|
289
|
+
"provider": str(spec.get("id") or "stripe"),
|
|
290
|
+
"redirect_url": f"https://connect.stripe.com/oauth/authorize?{params}",
|
|
291
|
+
"output": "",
|
|
292
|
+
"error": None,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _complete_stripe_oauth(user_id: str, code: str, state: str) -> dict[str, Any]:
|
|
297
|
+
pending = _take_oauth_pending(state)
|
|
298
|
+
if not pending or str(pending.get("user_id") or "") != user_id:
|
|
299
|
+
return {"ok": False, "error": "invalid_oauth_state"}
|
|
300
|
+
cfg = _stripe_connect_app_config()
|
|
301
|
+
client_secret = str(cfg.get("client_secret") or "").strip()
|
|
302
|
+
if not client_secret:
|
|
303
|
+
return {"ok": False, "error": "stripe_connect_not_configured"}
|
|
304
|
+
token_data = _stripe_exchange_code(code, client_secret)
|
|
305
|
+
if token_data.get("error"):
|
|
306
|
+
err = token_data.get("error")
|
|
307
|
+
if isinstance(err, dict):
|
|
308
|
+
return {"ok": False, "error": str(err.get("message") or err.get("description") or "stripe_oauth_failed")}
|
|
309
|
+
return {"ok": False, "error": str(err)}
|
|
310
|
+
access_token = str(token_data.get("access_token") or "").strip()
|
|
311
|
+
if not access_token:
|
|
312
|
+
return {"ok": False, "error": "stripe_missing_access_token"}
|
|
313
|
+
stripe_user_id = str(token_data.get("stripe_user_id") or "").strip()
|
|
314
|
+
spec = provider_catalog.catalog_provider("stripe") or {}
|
|
315
|
+
env_var = str(spec.get("env_var") or "STRIPE_SECRET_KEY")
|
|
316
|
+
label = f"Stripe Connect ({stripe_user_id})" if stripe_user_id else "Stripe Connect"
|
|
317
|
+
payload = _srv()._store_user_credential(user_id, "stripe", "oauth", access_token, env_var, label)
|
|
318
|
+
cred_ref = str(payload["credential_ref"])
|
|
319
|
+
now = _srv()._utc_now()
|
|
320
|
+
cred_id = str(uuid.uuid4())
|
|
321
|
+
try:
|
|
322
|
+
conn = sqlite3.connect(str(_srv()._workframe_db_path()), timeout=3.0)
|
|
323
|
+
conn.execute(
|
|
324
|
+
"""
|
|
325
|
+
INSERT INTO credential_bindings
|
|
326
|
+
(id, workspace_id, user_id, agent_profile_id, provider, credential_type,
|
|
327
|
+
credential_ref, label, is_active, created_by, created_at, updated_at)
|
|
328
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
|
329
|
+
""",
|
|
330
|
+
(
|
|
331
|
+
cred_id, None, user_id, None, "stripe", "oauth", cred_ref,
|
|
332
|
+
label, 1, user_id, now, now,
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
conn.commit()
|
|
336
|
+
conn.close()
|
|
337
|
+
except sqlite3.Error as exc:
|
|
338
|
+
return {"ok": False, "error": f"db_error: {exc}"}
|
|
339
|
+
return {"ok": True, "provider": "stripe", "credential_id": cred_id, "stripe_user_id": stripe_user_id}
|
|
340
|
+
def _start_discord_oauth(user_id: str, workspace_id: str = "") -> dict[str, Any]:
|
|
341
|
+
cfg = platform_auth.resolved_discord_oauth()
|
|
342
|
+
if not cfg.get("client_id") or not cfg.get("client_secret"):
|
|
343
|
+
return {
|
|
344
|
+
"ok": False,
|
|
345
|
+
"error": "discord_oauth_not_configured",
|
|
346
|
+
"message": "Workframe admin must register a Discord OAuth app under Integrations.",
|
|
347
|
+
}
|
|
348
|
+
state = secrets.token_urlsafe(24)
|
|
349
|
+
_store_oauth_pending(state, user_id, "discord", "", workspace_id)
|
|
350
|
+
params = urllib.parse.urlencode(
|
|
351
|
+
{
|
|
352
|
+
"client_id": cfg["client_id"],
|
|
353
|
+
"redirect_uri": platform_auth.discord_redirect_uri(),
|
|
354
|
+
"response_type": "code",
|
|
355
|
+
"scope": "identify",
|
|
356
|
+
"state": state,
|
|
357
|
+
"prompt": "consent",
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
return {
|
|
361
|
+
"ok": True,
|
|
362
|
+
"provider": "discord",
|
|
363
|
+
"redirect_url": f"https://discord.com/api/oauth2/authorize?{params}",
|
|
364
|
+
"output": "",
|
|
365
|
+
"error": None,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _complete_discord_oauth(user_id: str, code: str, state: str) -> dict[str, Any]:
|
|
370
|
+
pending = _take_oauth_pending(state)
|
|
371
|
+
if not pending or str(pending.get("user_id") or "") != user_id:
|
|
372
|
+
return {"ok": False, "error": "invalid_oauth_state"}
|
|
373
|
+
if str(pending.get("provider") or "") != "discord":
|
|
374
|
+
return {"ok": False, "error": "invalid_oauth_provider"}
|
|
375
|
+
result = platform_auth.complete_discord_link(str(code or ""))
|
|
376
|
+
if not result.get("ok"):
|
|
377
|
+
return result
|
|
378
|
+
patch = result.get("platform_ids") if isinstance(result.get("platform_ids"), dict) else {}
|
|
379
|
+
if patch:
|
|
380
|
+
_srv()._merge_user_platform_ids(user_id, {str(k): str(v) for k, v in patch.items()})
|
|
381
|
+
return {"ok": True, "provider": "discord", "platform_ids": patch}
|
|
@@ -12,7 +12,7 @@ _CACHE: dict[str, Any] = {"at": 0.0, "rows": []}
|
|
|
12
12
|
_TTL_SEC = int(__import__("os").environ.get("WORKFRAME_OPENROUTER_CATALOG_TTL", "3600"))
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def _fetch_models(api_key: str) -> list[dict[str, str]]:
|
|
15
|
+
def _fetch_models(api_key: str, *, timeout: float = 45) -> list[dict[str, str]]:
|
|
16
16
|
req = urllib.request.Request(
|
|
17
17
|
"https://openrouter.ai/api/v1/models",
|
|
18
18
|
headers={
|
|
@@ -22,7 +22,7 @@ def _fetch_models(api_key: str) -> list[dict[str, str]]:
|
|
|
22
22
|
},
|
|
23
23
|
method="GET",
|
|
24
24
|
)
|
|
25
|
-
with urllib.request.urlopen(req, timeout=
|
|
25
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
26
26
|
data = json.loads(resp.read())
|
|
27
27
|
rows: list[dict[str, str]] = []
|
|
28
28
|
for item in data.get("data") or []:
|
|
@@ -48,7 +48,7 @@ def _fetch_models(api_key: str) -> list[dict[str, str]]:
|
|
|
48
48
|
return rows[:120]
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def live_suggestions(api_key: str = "", *, limit: int = 40) -> list[dict[str, str]]:
|
|
51
|
+
def live_suggestions(api_key: str = "", *, limit: int = 40, timeout: float = 45) -> list[dict[str, str]]:
|
|
52
52
|
"""Return cached OpenRouter suggestions; empty without a key."""
|
|
53
53
|
key = str(api_key or "").strip()
|
|
54
54
|
if not key:
|
|
@@ -57,7 +57,7 @@ def live_suggestions(api_key: str = "", *, limit: int = 40) -> list[dict[str, st
|
|
|
57
57
|
if _CACHE["rows"] and now - float(_CACHE["at"]) < _TTL_SEC:
|
|
58
58
|
return list(_CACHE["rows"])[:limit]
|
|
59
59
|
try:
|
|
60
|
-
rows = _fetch_models(key)
|
|
60
|
+
rows = _fetch_models(key, timeout=timeout)
|
|
61
61
|
except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, OSError):
|
|
62
62
|
return list(_CACHE["rows"])[:limit]
|
|
63
63
|
_CACHE["at"] = now
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workframe/workframe-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "python3 server.py",
|
|
8
8
|
"start": "python3 server.py",
|
|
9
|
-
"typecheck": "
|
|
9
|
+
"typecheck": "python -m py_compile server.py zk_auth.py email_sender.py profile_config_yaml.py route_registry.py domain/entities.py domain/__init__.py && python test_public_routes.py && python test_route_registry.py && python test_billing_provider.py && python test_model_surface_consistency.py && python test_profile_model_yaml.py && python test_domain_entities.py",
|
|
10
10
|
"build": "python3 -m py_compile server.py zk_auth.py email_sender.py"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""WF-032 extract: ensure profile Hermes API is up (start/restart lifecycle)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import profile_gateway
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _srv():
|
|
11
|
+
import server as srv
|
|
12
|
+
|
|
13
|
+
return srv
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_profile_api(
|
|
17
|
+
profile: str,
|
|
18
|
+
user_id: str = "",
|
|
19
|
+
workspace_id: str = "",
|
|
20
|
+
*,
|
|
21
|
+
bootstrap_providers: bool = True,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Start profile gateway if down. ponytail: bind/chat must not mutate yaml or bootstrap creds."""
|
|
24
|
+
prof = _srv().resolve_hermes_profile(profile)
|
|
25
|
+
port = profile_gateway._profile_api_port(prof)
|
|
26
|
+
if profile_gateway._profile_api_healthy(prof):
|
|
27
|
+
return {"ok": True, "profile": prof, "api_port": port, "started": False}
|
|
28
|
+
# ponytail: gateway restart (~7s) before supervisor cold start (~37s).
|
|
29
|
+
if (
|
|
30
|
+
_srv()._is_runtime_profile_slug(prof)
|
|
31
|
+
and prof != _srv()._primary_profile()
|
|
32
|
+
and _srv()._runtime_profile_on_disk(prof)
|
|
33
|
+
):
|
|
34
|
+
with _srv()._gateway_lifecycle_lock:
|
|
35
|
+
if profile_gateway._profile_api_healthy(prof, use_cache=False):
|
|
36
|
+
return {"ok": True, "profile": prof, "api_port": port, "started": False}
|
|
37
|
+
code, _out = _srv()._gateway_exec(prof, ["gateway", "restart"])
|
|
38
|
+
if code == 0:
|
|
39
|
+
_srv()._invalidate_profile_health_cache(prof)
|
|
40
|
+
if profile_gateway._wait_profile_api_healthy(prof, attempts=48, delay=0.25):
|
|
41
|
+
return {"ok": True, "profile": prof, "api_port": port, "started": False}
|
|
42
|
+
if prof == _srv()._primary_profile():
|
|
43
|
+
ok, out, _port = profile_gateway._configure_profile_api(prof)
|
|
44
|
+
if not ok:
|
|
45
|
+
raise ValueError(f"profile api config failed: {out}")
|
|
46
|
+
_srv()._restart_stack_gateway()
|
|
47
|
+
if not profile_gateway._wait_profile_api_healthy(prof):
|
|
48
|
+
raise ValueError(f"profile api did not become healthy: {prof}")
|
|
49
|
+
return {"ok": True, "profile": prof, "api_port": port, "started": True}
|
|
50
|
+
if bootstrap_providers and user_id:
|
|
51
|
+
_srv()._bootstrap_profile_providers(prof, user_id, workspace_id)
|
|
52
|
+
with _srv()._gateway_lifecycle_lock:
|
|
53
|
+
if profile_gateway._profile_api_healthy(prof):
|
|
54
|
+
return {"ok": True, "profile": prof, "api_port": port, "started": False}
|
|
55
|
+
result = profile_gateway.profile_gateway_lifecycle(
|
|
56
|
+
prof, "start", bootstrap_providers=bootstrap_providers
|
|
57
|
+
)
|
|
58
|
+
if not profile_gateway._wait_profile_api_healthy(prof):
|
|
59
|
+
raise ValueError(f"profile api did not become healthy: {prof}")
|
|
60
|
+
return {
|
|
61
|
+
**result,
|
|
62
|
+
"ok": True,
|
|
63
|
+
"profile": prof,
|
|
64
|
+
"api_port": port,
|
|
65
|
+
"started": True,
|
|
66
|
+
}
|