create-workframe 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/SECURITY.md +4 -6
- package/bin/create-workframe.js +3 -3
- package/package.json +1 -1
- package/scripts/apply-update-hermes.sh +17 -17
- package/scripts/apply-update-workframe.sh +77 -77
- package/scripts/bootstrap-workspace-link.sh +8 -8
- package/scripts/compose-docker-host.sh +37 -37
- package/scripts/fix-zk-encryption-key.sh +35 -35
- package/scripts/restart-gateway-hermes.sh +12 -12
- package/scripts/setup-stack-secrets.sh +50 -50
- package/scripts/sync-canonical-to-package.mjs +19 -2
- package/scripts/verify-public-deploy.sh +105 -105
- package/workframe-api/README.md +10 -14
- package/workframe-api/concierge.py +173 -0
- package/workframe-api/llm_error_glossary.py +236 -0
- package/workframe-api/llm_proxy.py +3 -0
- package/workframe-api/openrouter_catalog.py +114 -0
- package/workframe-api/package.json +1 -1
- package/workframe-api/server.py +144 -48
- package/workframe-ui/public/assets/{arc-COAT3laO.js → arc-CRhulRPr.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CwxC-UJz.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BFjWV24l.js → architectureDiagram-3BPJPVTR-B1Gq5onc.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DSQLPfrj.js → blockDiagram-GPEHLZMM-DtXEetq-.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DKEHv1t2.js → c4Diagram-AAUBKEIU-Bw7NEN3_.js} +1 -1
- package/workframe-ui/public/assets/channel-BaZSQvmW.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-DHZg-DUi.js → chunk-2J33WTMH-91lObBre.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-BB-OYTfp.js → chunk-3OPIFGDE-D5IopWdW.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-C93q0YIm.js → chunk-4BX2VUAB-DRHbh4Yd.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-MAYniqik.js → chunk-55IACEB6-DcfKoh86.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-ChgN6YJs.js → chunk-5ZQYHXKU-BMtcWXOU.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-B_FYwdAv.js → chunk-727SXJPM-BW7tTfej.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-1_Hw_h1A.js → chunk-AQP2D5EJ-CGoSQAgm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CFiDQ1Rv.js → chunk-BSJP7CBP-RW1Jng9K.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-DZ9UMTlB.js → chunk-CSCIHK7Q-D8PwFfuF.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DlMlyFgw.js → chunk-FMBD7UC4-rUp_DSZ2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-DHXtQ_Hf.js → chunk-KSCS5N6A-C3eMeGne.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CuQzg-QG.js → chunk-L5ZTLDWV-DaE5LwXn.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-BHzjzCGg.js → chunk-LZXEDZCA-Cmn67nL3.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-DHXx05n2.js → chunk-ND2GUHAM-Bg-FPAN5.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CV5pmDM_.js → chunk-NZK2D7GU-orlL5R9g.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-6tkCHxsV.js → chunk-O5CBEL6O-BaPyh9SK.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-DXcsnu-x.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B2O0DZxu.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-BClwIiCp.js → chunk-XPW4576I-CXYFE8tT.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-C_TyUBYX.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-C_TyUBYX.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DOrGV6DQ.js → cose-bilkent-S5V4N54A-DDzKMTbp.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DXTPvJkX.js → dagre-BM42HDAG-DCa6EvS3.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-xX_v-pbf.js → diagram-2AECGRRQ-C-WIQoE_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-Cd2pXbBe.js → diagram-5GNKFQAL-UDk1nRH6.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df3XvUtk.js → diagram-KO2AKTUF-DjLxUTPx.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CsijIPaD.js → diagram-LMA3HP47-DNA9GSwJ.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-aq5fmfHd.js → diagram-OG6HWLK6-DO2nQu2f.js} +1 -1
- package/workframe-ui/public/assets/{dist-D1c0mkbB.js → dist-DyW8oFIW.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-DnFysVRY.js → erDiagram-TEJ5UH35-DFtJLZx1.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-BTmiKS74.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C6Ebi3su.js → flowDiagram-I6XJVG4X-B6K6EnPA.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-BQXQtUpa.js → ganttDiagram-6RSMTGT7-CbuWdupl.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-Dt0zIs_M.js → gitGraph-WXDBUCRP-BW_jwUPU.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BF8gHzRn.js → gitGraphDiagram-PVQCEYII-B4MYuP9j.js} +1 -1
- package/workframe-ui/public/assets/index-BYacg3Nj.js +133 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CSmszQJT.js → info-J43DQDTF-Xa61PMDn.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CVTKGW6p.js → infoDiagram-5YYISTIA-BNbQgdUG.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js → ishikawaDiagram-YF4QCWOH-BDfiCzHo.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-r3wD68_T.js → journeyDiagram-JHISSGLW-Dn52B8tT.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Il8VglqN.js → kanban-definition-UN3LZRKU-rhwkS_g6.js} +1 -1
- package/workframe-ui/public/assets/{line-oyjpfz2A.js → line-BQJgrj-x.js} +1 -1
- package/workframe-ui/public/assets/{linear-Cf7p5tVp.js → linear-Bq2ZvWSJ.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-YmbZ-AfY.js → mermaid-parser.core-CwFK_i8p.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-BFdCAqCo.js → mermaid.core-C9CAm83c.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Cy2iCtEl.js → mindmap-definition-RKZ34NQL-BXwEccNZ.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-DwOBZL6K.js → packet-YPE3B663-D754iSKT.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-04PPhnKK.js → pie-LRSECV5Y-BQ2V4UWk.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-LxIpgHqi.js → pieDiagram-4H26LBE5-CNHecl4C.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-0nBYfYm4.js → quadrantDiagram-W4KKPZXB-Bu6ciL2O.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-D2-vBqps.js → radar-GUYGQ44K-Dr8b9Qak.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DbuU0nlu.js → requirementDiagram-4Y6WPE33-Beua6Xju.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-B2hQ6B2x.js → sankeyDiagram-5OEKKPKP-CSDFJ0P_.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BBrU30e1.js → sequenceDiagram-3UESZ5HK-KKziodNI.js} +1 -1
- package/workframe-ui/public/assets/{src-BJEDmV70.js → src-PKTIkQR0.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-7FGO4kkH.js → stateDiagram-AJRCARHV-Bu8FNMcj.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-E9A5RCa9.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-ptDm4rCN.js → timeline-definition-PNZ67QCA-DMa-RPqy.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CS6Z-0q8.js → treeView-BLDUP644-CzQeUY7D.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DqV4Y2VA.js → treemap-LRROVOQU-BT4RYAhY.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-C0UrZJYt.js → vennDiagram-CIIHVFJN-nKaUH6GG.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-bNDN3_Sa.js → wardley-L42UT6IY-eSpvH7au.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-jWiJsefM.js → wardleyDiagram-YWT4CUSO-B5u4s4a5.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-Dsh_fLCy.js → xychartDiagram-2RQKCTM6-DcXo5hDi.js} +1 -1
- package/workframe-ui/public/index.html +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +0 -1
- package/workframe-ui/public/assets/channel-g7r_RGaY.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +0 -1
- package/workframe-ui/public/assets/index-lRpzpNPT.js +0 -133
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +0 -1
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fail-closed preflight for WORKFRAME_DEPLOYMENT_MODE=public_multi_user.
|
|
3
|
-
# Usage: bash scripts/workframe/verify-public-deploy.sh [compose-dir]
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
7
|
-
COMPOSE_DIR="${1:-$ROOT/infra/compose/workframe}"
|
|
8
|
-
ENV_FILE="$COMPOSE_DIR/.env"
|
|
9
|
-
COMPOSE_FILE="$COMPOSE_DIR/docker-compose.yml"
|
|
10
|
-
|
|
11
|
-
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
12
|
-
warn() { echo "WARN: $*" >&2; }
|
|
13
|
-
ok() { echo "OK: $*"; }
|
|
14
|
-
|
|
15
|
-
[[ -f "$ENV_FILE" ]] || fail "missing $ENV_FILE"
|
|
16
|
-
[[ -f "$COMPOSE_FILE" ]] || fail "missing $COMPOSE_FILE"
|
|
17
|
-
|
|
18
|
-
env_val() {
|
|
19
|
-
local key="$1"
|
|
20
|
-
grep -E "^${key}=" "$ENV_FILE" | tail -n1 | cut -d= -f2- | tr -d '\r' || true
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
MODE="$(env_val WORKFRAME_DEPLOYMENT_MODE)"
|
|
24
|
-
MODE="${MODE:-trusted_team}"
|
|
25
|
-
if [[ "$MODE" != "public_multi_user" ]]; then
|
|
26
|
-
ok "WORKFRAME_DEPLOYMENT_MODE=$MODE (skipping public checks; pass nothing to force public)"
|
|
27
|
-
exit 0
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
ok "checking public_multi_user preflight"
|
|
31
|
-
|
|
32
|
-
if [[ "$(env_val DEV_LOCAL_UNSAFE)" =~ ^(1|true|yes|on)$ ]]; then
|
|
33
|
-
fail "DEV_LOCAL_UNSAFE must be off for public_multi_user"
|
|
34
|
-
fi
|
|
35
|
-
if [[ "$(env_val SECURE_MODE)" != "true" ]]; then
|
|
36
|
-
fail "SECURE_MODE=true required"
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
for key in WORKFRAME_SUPERVISOR_TOKEN WORKFRAME_API_TOKEN WORKFRAME_PROXY_TOKEN WORKFRAME_VAULT_KEK \
|
|
40
|
-
ZK_AUTH_HMAC_KEY ZK_AUTH_ENCRYPTION_KEY ZK_AUTH_SESSION_SECRET \
|
|
41
|
-
SMTP_HOST SMTP_USER SMTP_PASS EMAIL_FROM; do
|
|
42
|
-
[[ -n "$(env_val "$key")" ]] || fail "$key is empty"
|
|
43
|
-
done
|
|
44
|
-
|
|
45
|
-
APP_URL="$(env_val APP_BASE_URL)"
|
|
46
|
-
[[ "$APP_URL" == https://* ]] || fail "APP_BASE_URL must be https:// (got ${APP_URL:-<empty>})"
|
|
47
|
-
|
|
48
|
-
enc_key="$(env_val ZK_AUTH_ENCRYPTION_KEY)"
|
|
49
|
-
if [[ -n "$enc_key" ]]; then
|
|
50
|
-
python3 - "$enc_key" <<'PY' || fail "ZK_AUTH_ENCRYPTION_KEY must be base64-encoded 32 bytes (not hex)"
|
|
51
|
-
import base64, sys
|
|
52
|
-
raw = base64.b64decode(sys.argv[1].strip())
|
|
53
|
-
assert len(raw) == 32
|
|
54
|
-
PY
|
|
55
|
-
ok "ZK_AUTH_ENCRYPTION_KEY format"
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
if [[ "$(env_val WORKFRAME_E2E)" =~ ^(1|true|yes|on)$ ]] && [[ "$APP_URL" == https://* ]]; then
|
|
59
|
-
fail "WORKFRAME_E2E must be off for public HTTPS deploy"
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
if grep -A40 '^ gateway:' "$COMPOSE_FILE" | grep -q 'env_file:'; then
|
|
63
|
-
fail "gateway must not use env_file in docker-compose.yml"
|
|
64
|
-
fi
|
|
65
|
-
if ! grep -q 'control-net' "$COMPOSE_FILE"; then
|
|
66
|
-
fail "control-net missing from docker-compose.yml"
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
UI_PORT="$(env_val WORKFRAME_UI_PORT)"
|
|
70
|
-
UI_PORT="${UI_PORT:-18644}"
|
|
71
|
-
API_PORT="$(env_val WORKFRAME_API_PORT)"
|
|
72
|
-
API_PORT="${API_PORT:-19120}"
|
|
73
|
-
|
|
74
|
-
if command -v curl >/dev/null 2>&1; then
|
|
75
|
-
health="$(curl -fsS "http://127.0.0.1:${API_PORT}/api/health" 2>/dev/null || true)"
|
|
76
|
-
if [[ -z "$health" ]]; then
|
|
77
|
-
warn "API health unreachable on 127.0.0.1:${API_PORT} (stack down?)"
|
|
78
|
-
else
|
|
79
|
-
echo "$health" | grep -q 'public_multi_user' || fail "API health missing deployment_mode public_multi_user"
|
|
80
|
-
echo "$health" | grep -q '"mode": "secure"' || fail "API not in secure mode"
|
|
81
|
-
echo "$health" | grep -q '"proxy_token_configured": true' || fail "API proxy_token_configured is false"
|
|
82
|
-
echo "$health" | grep -q '"vault_envelope": true' || fail "API vault_envelope is false"
|
|
83
|
-
echo "$health" | grep -q '"docker_sock_on_api": false' || fail "API still has docker.sock mounted"
|
|
84
|
-
echo "$health" | grep -q 'install_window_open' || warn "API health missing install_window_open"
|
|
85
|
-
echo "$health" | grep -q 'workframe_e2e' || warn "API health missing workframe_e2e"
|
|
86
|
-
echo "$health" | grep -q 'dev_local_unsafe' || warn "API health missing dev_local_unsafe"
|
|
87
|
-
ok "API health"
|
|
88
|
-
fi
|
|
89
|
-
dash_code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${UI_PORT}/hermes-dashboard/" || true)"
|
|
90
|
-
[[ "$dash_code" == "403" ]] || warn "hermes-dashboard without session returned $dash_code (expected 403)"
|
|
91
|
-
else
|
|
92
|
-
warn "curl not found — skipping HTTP checks"
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
if command -v docker >/dev/null 2>&1 && docker inspect workframe-gateway >/dev/null 2>&1; then
|
|
96
|
-
gw_env="$(docker inspect workframe-gateway --format '{{range .Config.Env}}{{println .}}{{end}}')"
|
|
97
|
-
for marker in WORKFRAME_SUPERVISOR_TOKEN ZK_AUTH_ SMTP_PASS; do
|
|
98
|
-
echo "$gw_env" | grep -q "$marker" && fail "gateway env contains $marker"
|
|
99
|
-
done
|
|
100
|
-
ok "gateway env allowlist"
|
|
101
|
-
else
|
|
102
|
-
warn "workframe-gateway not running — skipping docker inspect"
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
ok "public_multi_user preflight passed"
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fail-closed preflight for WORKFRAME_DEPLOYMENT_MODE=public_multi_user.
|
|
3
|
+
# Usage: bash scripts/workframe/verify-public-deploy.sh [compose-dir]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
7
|
+
COMPOSE_DIR="${1:-$ROOT/infra/compose/workframe}"
|
|
8
|
+
ENV_FILE="$COMPOSE_DIR/.env"
|
|
9
|
+
COMPOSE_FILE="$COMPOSE_DIR/docker-compose.yml"
|
|
10
|
+
|
|
11
|
+
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
12
|
+
warn() { echo "WARN: $*" >&2; }
|
|
13
|
+
ok() { echo "OK: $*"; }
|
|
14
|
+
|
|
15
|
+
[[ -f "$ENV_FILE" ]] || fail "missing $ENV_FILE"
|
|
16
|
+
[[ -f "$COMPOSE_FILE" ]] || fail "missing $COMPOSE_FILE"
|
|
17
|
+
|
|
18
|
+
env_val() {
|
|
19
|
+
local key="$1"
|
|
20
|
+
grep -E "^${key}=" "$ENV_FILE" | tail -n1 | cut -d= -f2- | tr -d '\r' || true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
MODE="$(env_val WORKFRAME_DEPLOYMENT_MODE)"
|
|
24
|
+
MODE="${MODE:-trusted_team}"
|
|
25
|
+
if [[ "$MODE" != "public_multi_user" ]]; then
|
|
26
|
+
ok "WORKFRAME_DEPLOYMENT_MODE=$MODE (skipping public checks; pass nothing to force public)"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
ok "checking public_multi_user preflight"
|
|
31
|
+
|
|
32
|
+
if [[ "$(env_val DEV_LOCAL_UNSAFE)" =~ ^(1|true|yes|on)$ ]]; then
|
|
33
|
+
fail "DEV_LOCAL_UNSAFE must be off for public_multi_user"
|
|
34
|
+
fi
|
|
35
|
+
if [[ "$(env_val SECURE_MODE)" != "true" ]]; then
|
|
36
|
+
fail "SECURE_MODE=true required"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
for key in WORKFRAME_SUPERVISOR_TOKEN WORKFRAME_API_TOKEN WORKFRAME_PROXY_TOKEN WORKFRAME_VAULT_KEK \
|
|
40
|
+
ZK_AUTH_HMAC_KEY ZK_AUTH_ENCRYPTION_KEY ZK_AUTH_SESSION_SECRET \
|
|
41
|
+
SMTP_HOST SMTP_USER SMTP_PASS EMAIL_FROM; do
|
|
42
|
+
[[ -n "$(env_val "$key")" ]] || fail "$key is empty"
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
APP_URL="$(env_val APP_BASE_URL)"
|
|
46
|
+
[[ "$APP_URL" == https://* ]] || fail "APP_BASE_URL must be https:// (got ${APP_URL:-<empty>})"
|
|
47
|
+
|
|
48
|
+
enc_key="$(env_val ZK_AUTH_ENCRYPTION_KEY)"
|
|
49
|
+
if [[ -n "$enc_key" ]]; then
|
|
50
|
+
python3 - "$enc_key" <<'PY' || fail "ZK_AUTH_ENCRYPTION_KEY must be base64-encoded 32 bytes (not hex)"
|
|
51
|
+
import base64, sys
|
|
52
|
+
raw = base64.b64decode(sys.argv[1].strip())
|
|
53
|
+
assert len(raw) == 32
|
|
54
|
+
PY
|
|
55
|
+
ok "ZK_AUTH_ENCRYPTION_KEY format"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ "$(env_val WORKFRAME_E2E)" =~ ^(1|true|yes|on)$ ]] && [[ "$APP_URL" == https://* ]]; then
|
|
59
|
+
fail "WORKFRAME_E2E must be off for public HTTPS deploy"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if grep -A40 '^ gateway:' "$COMPOSE_FILE" | grep -q 'env_file:'; then
|
|
63
|
+
fail "gateway must not use env_file in docker-compose.yml"
|
|
64
|
+
fi
|
|
65
|
+
if ! grep -q 'control-net' "$COMPOSE_FILE"; then
|
|
66
|
+
fail "control-net missing from docker-compose.yml"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
UI_PORT="$(env_val WORKFRAME_UI_PORT)"
|
|
70
|
+
UI_PORT="${UI_PORT:-18644}"
|
|
71
|
+
API_PORT="$(env_val WORKFRAME_API_PORT)"
|
|
72
|
+
API_PORT="${API_PORT:-19120}"
|
|
73
|
+
|
|
74
|
+
if command -v curl >/dev/null 2>&1; then
|
|
75
|
+
health="$(curl -fsS "http://127.0.0.1:${API_PORT}/api/health" 2>/dev/null || true)"
|
|
76
|
+
if [[ -z "$health" ]]; then
|
|
77
|
+
warn "API health unreachable on 127.0.0.1:${API_PORT} (stack down?)"
|
|
78
|
+
else
|
|
79
|
+
echo "$health" | grep -q 'public_multi_user' || fail "API health missing deployment_mode public_multi_user"
|
|
80
|
+
echo "$health" | grep -q '"mode": "secure"' || fail "API not in secure mode"
|
|
81
|
+
echo "$health" | grep -q '"proxy_token_configured": true' || fail "API proxy_token_configured is false"
|
|
82
|
+
echo "$health" | grep -q '"vault_envelope": true' || fail "API vault_envelope is false"
|
|
83
|
+
echo "$health" | grep -q '"docker_sock_on_api": false' || fail "API still has docker.sock mounted"
|
|
84
|
+
echo "$health" | grep -q 'install_window_open' || warn "API health missing install_window_open"
|
|
85
|
+
echo "$health" | grep -q 'workframe_e2e' || warn "API health missing workframe_e2e"
|
|
86
|
+
echo "$health" | grep -q 'dev_local_unsafe' || warn "API health missing dev_local_unsafe"
|
|
87
|
+
ok "API health"
|
|
88
|
+
fi
|
|
89
|
+
dash_code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${UI_PORT}/hermes-dashboard/" || true)"
|
|
90
|
+
[[ "$dash_code" == "403" ]] || warn "hermes-dashboard without session returned $dash_code (expected 403)"
|
|
91
|
+
else
|
|
92
|
+
warn "curl not found — skipping HTTP checks"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if command -v docker >/dev/null 2>&1 && docker inspect workframe-gateway >/dev/null 2>&1; then
|
|
96
|
+
gw_env="$(docker inspect workframe-gateway --format '{{range .Config.Env}}{{println .}}{{end}}')"
|
|
97
|
+
for marker in WORKFRAME_SUPERVISOR_TOKEN ZK_AUTH_ SMTP_PASS; do
|
|
98
|
+
echo "$gw_env" | grep -q "$marker" && fail "gateway env contains $marker"
|
|
99
|
+
done
|
|
100
|
+
ok "gateway env allowlist"
|
|
101
|
+
else
|
|
102
|
+
warn "workframe-gateway not running — skipping docker inspect"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
ok "public_multi_user preflight passed"
|
package/workframe-api/README.md
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
# Workframe API Service
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HTTP API for the Workframe UI. See [docs/VERSION.md](../../docs/VERSION.md) for release version.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This is intentionally preserved as the current Python BFF instead of being rewritten into `apps/api`.
|
|
8
|
-
|
|
9
|
-
## Local
|
|
5
|
+
## Local (outside Docker)
|
|
10
6
|
|
|
11
7
|
```bash
|
|
12
8
|
cd services/workframe-api
|
|
13
9
|
python3 -m venv .venv
|
|
14
|
-
. .venv/bin/activate
|
|
10
|
+
. .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
15
11
|
pip install -r requirements.txt
|
|
16
12
|
HOST=0.0.0.0 PORT=8080 HERMES_DATA=/opt/data WORKSPACE=/workspace python3 server.py
|
|
17
13
|
```
|
|
18
14
|
|
|
19
|
-
## Runtime state
|
|
15
|
+
## Runtime state (not committed)
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
- `data/*.db`, vault files under `WORKFRAME_API_DATA_DIR`
|
|
18
|
+
- Hermes `Agents/` tree on mounted volume
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
- `data/.auth_keys`
|
|
25
|
-
- Hermes `Agents/`
|
|
26
|
-
- workspace `Files/`
|
|
20
|
+
## Docs
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
- [Operations](../../docs/public/operations.md)
|
|
23
|
+
- [Security](../../docs/public/security.md)
|
|
24
|
+
- [API reference](../../docs/public/api-reference.md)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Deterministic concierge when LLM is unavailable — keyword intent router, no LLM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import llm_error_glossary as glossary
|
|
9
|
+
|
|
10
|
+
# ponytail: (intent_id, patterns) — first match wins
|
|
11
|
+
_INTENTS: list[tuple[str, tuple[str, ...]]] = [
|
|
12
|
+
(
|
|
13
|
+
"add_provider_keys",
|
|
14
|
+
(
|
|
15
|
+
"add key",
|
|
16
|
+
"add keys",
|
|
17
|
+
"api key",
|
|
18
|
+
"connect openrouter",
|
|
19
|
+
"connect provider",
|
|
20
|
+
"where key",
|
|
21
|
+
"how do i connect",
|
|
22
|
+
"how to connect",
|
|
23
|
+
"openrouter key",
|
|
24
|
+
"llm key",
|
|
25
|
+
"provider key",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
(
|
|
29
|
+
"change_model",
|
|
30
|
+
(
|
|
31
|
+
"set model",
|
|
32
|
+
"change model",
|
|
33
|
+
"pick model",
|
|
34
|
+
"which model",
|
|
35
|
+
"how do i set model",
|
|
36
|
+
"how to set model",
|
|
37
|
+
"choose model",
|
|
38
|
+
"switch model",
|
|
39
|
+
"model picker",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
"provider_credits",
|
|
44
|
+
(
|
|
45
|
+
"out of credit",
|
|
46
|
+
"no credit",
|
|
47
|
+
"add credit",
|
|
48
|
+
"billing",
|
|
49
|
+
"top up",
|
|
50
|
+
"purchase credit",
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
"invalid_key",
|
|
55
|
+
(
|
|
56
|
+
"invalid key",
|
|
57
|
+
"key rejected",
|
|
58
|
+
"bad key",
|
|
59
|
+
"wrong key",
|
|
60
|
+
"401",
|
|
61
|
+
"unauthorized",
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"diagnose",
|
|
66
|
+
(
|
|
67
|
+
"what's wrong",
|
|
68
|
+
"whats wrong",
|
|
69
|
+
"why no reply",
|
|
70
|
+
"not working",
|
|
71
|
+
"not replying",
|
|
72
|
+
"help me",
|
|
73
|
+
"what happened",
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"getting_started",
|
|
78
|
+
(
|
|
79
|
+
"get started",
|
|
80
|
+
"how do i start",
|
|
81
|
+
"how to start",
|
|
82
|
+
"new here",
|
|
83
|
+
"first time",
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
_INTENT_PLAYBOOK: dict[str, dict[str, Any]] = {
|
|
89
|
+
"add_provider_keys": {
|
|
90
|
+
"message": "Connect an LLM provider to enable chat.",
|
|
91
|
+
"hint": "OpenRouter is the fastest path — paste your API key under Connected accounts.",
|
|
92
|
+
"action": {"type": "open_settings", "tab": "providers"},
|
|
93
|
+
"action_label": "Add API key",
|
|
94
|
+
"code": "concierge_add_keys",
|
|
95
|
+
},
|
|
96
|
+
"change_model": {
|
|
97
|
+
"message": "Choose which model this agent uses.",
|
|
98
|
+
"hint": "Open the model picker and select a model your provider supports.",
|
|
99
|
+
"action": {"type": "open_settings", "tab": "model"},
|
|
100
|
+
"action_label": "Choose model",
|
|
101
|
+
"code": "concierge_change_model",
|
|
102
|
+
},
|
|
103
|
+
"provider_credits": glossary.playbook_entry("provider_no_credits"),
|
|
104
|
+
"invalid_key": glossary.playbook_entry("provider_invalid_key"),
|
|
105
|
+
"diagnose": {
|
|
106
|
+
"message": "I can help you troubleshoot setup.",
|
|
107
|
+
"hint": "Connect a provider first, then pick a live model. If chat still fails, check credits and the model name.",
|
|
108
|
+
"action": {"type": "open_settings", "tab": "providers"},
|
|
109
|
+
"action_label": "Connected accounts",
|
|
110
|
+
"secondary_action": {"type": "open_settings", "tab": "model"},
|
|
111
|
+
"secondary_action_label": "Choose model",
|
|
112
|
+
"code": "concierge_diagnose",
|
|
113
|
+
},
|
|
114
|
+
"getting_started": {
|
|
115
|
+
"message": "Welcome to Workframe. Three steps to your first agent reply:",
|
|
116
|
+
"hint": "1) Connect an LLM provider · 2) Pick a model · 3) Send a message in chat.",
|
|
117
|
+
"action": {"type": "open_settings", "tab": "providers"},
|
|
118
|
+
"action_label": "Connect provider",
|
|
119
|
+
"code": "concierge_getting_started",
|
|
120
|
+
},
|
|
121
|
+
"fallback_help": glossary.playbook_entry("no_llm_provider"),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _normalize(text: str) -> str:
|
|
126
|
+
return re.sub(r"\s+", " ", str(text or "").lower().strip())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def match_intent(user_text: str) -> str:
|
|
130
|
+
norm = _normalize(user_text)
|
|
131
|
+
if not norm:
|
|
132
|
+
return "fallback_help"
|
|
133
|
+
for intent_id, patterns in _INTENTS:
|
|
134
|
+
for pat in patterns:
|
|
135
|
+
if pat in norm:
|
|
136
|
+
return intent_id
|
|
137
|
+
return "fallback_help"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def respond(
|
|
141
|
+
user_text: str,
|
|
142
|
+
*,
|
|
143
|
+
situation: str = "",
|
|
144
|
+
last_error: dict[str, Any] | None = None,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Return concierge playbook for user message or classified error."""
|
|
147
|
+
if last_error and str(last_error.get("code") or "") not in ("", "concierge"):
|
|
148
|
+
entry = dict(last_error)
|
|
149
|
+
entry.setdefault("message", entry.get("text") or "")
|
|
150
|
+
hint = str(entry.get("hint") or "").strip()
|
|
151
|
+
if hint:
|
|
152
|
+
entry["hint"] = f"{hint} You can also ask how to connect keys or change models."
|
|
153
|
+
return glossary.notice_payload(entry)
|
|
154
|
+
|
|
155
|
+
if situation == "no_provider":
|
|
156
|
+
intent = match_intent(user_text)
|
|
157
|
+
if intent == "fallback_help":
|
|
158
|
+
return glossary.notice_payload(_INTENT_PLAYBOOK["getting_started"])
|
|
159
|
+
return glossary.notice_payload(_INTENT_PLAYBOOK.get(intent) or _INTENT_PLAYBOOK["fallback_help"])
|
|
160
|
+
|
|
161
|
+
intent = match_intent(user_text)
|
|
162
|
+
row = dict(_INTENT_PLAYBOOK.get(intent) or _INTENT_PLAYBOOK["fallback_help"])
|
|
163
|
+
return glossary.notice_payload(row)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
r = respond("how do I add keys?", situation="no_provider")
|
|
168
|
+
assert r["code"] == "concierge_add_keys"
|
|
169
|
+
r2 = respond("how do I set model?", situation="no_provider")
|
|
170
|
+
assert r2["code"] == "concierge_change_model"
|
|
171
|
+
r3 = respond("hello", situation="no_provider")
|
|
172
|
+
assert r3["code"] == "concierge_getting_started"
|
|
173
|
+
print("concierge ok")
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Classify LLM/provider failures into didactic copy + UI actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
# ponytail: static playbook — extend rows, no LLM generation
|
|
10
|
+
_ACTIONS: dict[str, dict[str, Any]] = {
|
|
11
|
+
"open_providers": {"type": "open_settings", "tab": "providers"},
|
|
12
|
+
"open_model": {"type": "open_settings", "tab": "model"},
|
|
13
|
+
"retry": {"type": "retry"},
|
|
14
|
+
"openrouter_credits": {
|
|
15
|
+
"type": "external_link",
|
|
16
|
+
"url": "https://openrouter.ai/credits",
|
|
17
|
+
"label": "Add credits",
|
|
18
|
+
},
|
|
19
|
+
"openrouter_keys": {
|
|
20
|
+
"type": "external_link",
|
|
21
|
+
"url": "https://openrouter.ai/keys",
|
|
22
|
+
"label": "Manage keys",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
PLAYBOOK: dict[str, dict[str, Any]] = {
|
|
27
|
+
"no_llm_provider": {
|
|
28
|
+
"message": "No model provider is connected for your account yet.",
|
|
29
|
+
"hint": "Connect OpenRouter or another LLM provider under Connected accounts, then try again.",
|
|
30
|
+
"action": _ACTIONS["open_providers"],
|
|
31
|
+
"action_label": "Connect provider",
|
|
32
|
+
},
|
|
33
|
+
"provider_invalid_key": {
|
|
34
|
+
"message": "Your API key was rejected by the provider.",
|
|
35
|
+
"hint": "Reset the key at your provider, then paste it again under Connected accounts.",
|
|
36
|
+
"action": _ACTIONS["open_providers"],
|
|
37
|
+
"action_label": "Update API key",
|
|
38
|
+
"secondary_action": _ACTIONS["openrouter_keys"],
|
|
39
|
+
"secondary_action_label": "Open provider keys",
|
|
40
|
+
},
|
|
41
|
+
"provider_no_credits": {
|
|
42
|
+
"message": "Your provider account has no credits remaining.",
|
|
43
|
+
"hint": "Add credits at your provider billing page, then send your message again.",
|
|
44
|
+
"action": _ACTIONS["openrouter_credits"],
|
|
45
|
+
"action_label": "Add credits",
|
|
46
|
+
"secondary_action": _ACTIONS["open_model"],
|
|
47
|
+
"secondary_action_label": "Choose another model",
|
|
48
|
+
},
|
|
49
|
+
"model_unavailable": {
|
|
50
|
+
"message": "The selected model is not available on your provider right now.",
|
|
51
|
+
"hint": "Pick a different model in Settings. Free models on OpenRouter rotate often.",
|
|
52
|
+
"action": _ACTIONS["open_model"],
|
|
53
|
+
"action_label": "Choose model",
|
|
54
|
+
},
|
|
55
|
+
"model_invalid_id": {
|
|
56
|
+
"message": "That model ID is not valid for your provider.",
|
|
57
|
+
"hint": "Open the model picker and select a model from the list, or type a known provider/model id.",
|
|
58
|
+
"action": _ACTIONS["open_model"],
|
|
59
|
+
"action_label": "Choose model",
|
|
60
|
+
},
|
|
61
|
+
"provider_rate_limited": {
|
|
62
|
+
"message": "The provider rate-limited this request.",
|
|
63
|
+
"hint": "Wait a minute or switch to another model.",
|
|
64
|
+
"action": _ACTIONS["open_model"],
|
|
65
|
+
"action_label": "Choose model",
|
|
66
|
+
},
|
|
67
|
+
"invalid_lease": {
|
|
68
|
+
"message": "Your session credential expired before the model replied.",
|
|
69
|
+
"hint": "Send the message again — Workframe will issue a fresh credential.",
|
|
70
|
+
"action": _ACTIONS["retry"],
|
|
71
|
+
"action_label": "Try again",
|
|
72
|
+
},
|
|
73
|
+
"provider_empty_reply": {
|
|
74
|
+
"message": "The model provider returned no reply.",
|
|
75
|
+
"hint": "Check your API key, credits, and model selection in Settings.",
|
|
76
|
+
"action": _ACTIONS["open_model"],
|
|
77
|
+
"action_label": "Check model",
|
|
78
|
+
"secondary_action": _ACTIONS["open_providers"],
|
|
79
|
+
"secondary_action_label": "Check API key",
|
|
80
|
+
},
|
|
81
|
+
"provider_gateway_error": {
|
|
82
|
+
"message": "Workframe could not reach the model gateway.",
|
|
83
|
+
"hint": "Confirm the stack is running and try again.",
|
|
84
|
+
"action": _ACTIONS["retry"],
|
|
85
|
+
"action_label": "Try again",
|
|
86
|
+
},
|
|
87
|
+
"unknown_provider_error": {
|
|
88
|
+
"message": "The model request failed.",
|
|
89
|
+
"hint": "Check Connected accounts and the model in Settings.",
|
|
90
|
+
"action": _ACTIONS["open_providers"],
|
|
91
|
+
"action_label": "Connected accounts",
|
|
92
|
+
"secondary_action": _ACTIONS["open_model"],
|
|
93
|
+
"secondary_action_label": "Choose model",
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def playbook_entry(code: str, **overrides: Any) -> dict[str, Any]:
|
|
99
|
+
"""Return a copy of a playbook row with optional string format overrides."""
|
|
100
|
+
base = dict(PLAYBOOK.get(code) or PLAYBOOK["unknown_provider_error"])
|
|
101
|
+
base["code"] = code
|
|
102
|
+
for key, value in overrides.items():
|
|
103
|
+
if value is not None and key in base and isinstance(base[key], str):
|
|
104
|
+
try:
|
|
105
|
+
base[key] = base[key].format(**overrides)
|
|
106
|
+
except (KeyError, ValueError):
|
|
107
|
+
base[key] = str(value)
|
|
108
|
+
elif value is not None:
|
|
109
|
+
base[key] = value
|
|
110
|
+
return base
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_json_blob(raw: str) -> dict[str, Any] | None:
|
|
114
|
+
text = str(raw or "").strip()
|
|
115
|
+
if not text or text[0] not in "{[":
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(text)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
return None
|
|
121
|
+
return data if isinstance(data, dict) else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_error_message(data: dict[str, Any] | None, raw: str = "") -> str:
|
|
125
|
+
if data:
|
|
126
|
+
err = data.get("error")
|
|
127
|
+
if isinstance(err, dict):
|
|
128
|
+
msg = str(err.get("message") or "").strip()
|
|
129
|
+
if msg:
|
|
130
|
+
return msg
|
|
131
|
+
if isinstance(err, str) and err.strip():
|
|
132
|
+
return err.strip()
|
|
133
|
+
for key in ("message", "detail"):
|
|
134
|
+
val = data.get(key)
|
|
135
|
+
if isinstance(val, str) and val.strip():
|
|
136
|
+
return val.strip()
|
|
137
|
+
return str(raw or "").strip()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _model_from_text(text: str) -> str:
|
|
141
|
+
m = re.search(r"for\s+([a-z0-9_.:/-]+)", text, re.I)
|
|
142
|
+
return m.group(1) if m else ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def classify_upstream(
|
|
146
|
+
status: int,
|
|
147
|
+
body: bytes | str | None = None,
|
|
148
|
+
*,
|
|
149
|
+
provider: str = "openrouter",
|
|
150
|
+
model: str = "",
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""Map upstream HTTP status + body to playbook entry."""
|
|
153
|
+
raw = body.decode("utf-8", errors="replace") if isinstance(body, (bytes, bytearray)) else str(body or "")
|
|
154
|
+
data = _parse_json_blob(raw)
|
|
155
|
+
msg = _extract_error_message(data, raw)
|
|
156
|
+
low = msg.lower()
|
|
157
|
+
model_id = model or _model_from_text(msg)
|
|
158
|
+
|
|
159
|
+
if status == 401 or "invalid api key" in low or "incorrect api key" in low or "unauthorized" in low:
|
|
160
|
+
return playbook_entry("provider_invalid_key", provider=provider)
|
|
161
|
+
if status == 402 or "insufficient" in low and "credit" in low or "payment required" in low:
|
|
162
|
+
return playbook_entry("provider_no_credits", provider=provider)
|
|
163
|
+
if status == 429 or "rate limit" in low:
|
|
164
|
+
return playbook_entry("provider_rate_limited", provider=provider)
|
|
165
|
+
if status == 400 and ("not a valid model" in low or "invalid model" in low):
|
|
166
|
+
entry = playbook_entry("model_invalid_id", model=model_id or "selected model")
|
|
167
|
+
if model_id:
|
|
168
|
+
entry["message"] = f"Model `{model_id}` is not a valid id on {provider}."
|
|
169
|
+
return entry
|
|
170
|
+
if status == 404 or "no endpoints found" in low or "model not found" in low:
|
|
171
|
+
entry = playbook_entry("model_unavailable", model=model_id or "selected model")
|
|
172
|
+
if model_id:
|
|
173
|
+
entry["message"] = f"Model `{model_id}` has no live endpoints on {provider} right now."
|
|
174
|
+
return entry
|
|
175
|
+
if status >= 500:
|
|
176
|
+
return playbook_entry("provider_gateway_error", provider=provider)
|
|
177
|
+
return playbook_entry("unknown_provider_error", provider=provider, detail=msg[:200] if msg else "")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def classify_exception_text(text: str) -> dict[str, Any]:
|
|
181
|
+
raw = str(text or "").strip()
|
|
182
|
+
if not raw:
|
|
183
|
+
return playbook_entry("provider_empty_reply")
|
|
184
|
+
if "no_llm_provider_for_user" in raw:
|
|
185
|
+
return playbook_entry("no_llm_provider")
|
|
186
|
+
if "invalid_lease" in raw or "credential expired" in raw.lower():
|
|
187
|
+
return playbook_entry("invalid_lease")
|
|
188
|
+
m = re.search(r"HTTP (\d{3})", raw)
|
|
189
|
+
if m:
|
|
190
|
+
return classify_upstream(int(m.group(1)), raw)
|
|
191
|
+
low = raw.lower()
|
|
192
|
+
if "no endpoints found" in low or "model" in low and "not found" in low:
|
|
193
|
+
return classify_upstream(404, raw)
|
|
194
|
+
if "not a valid model" in low:
|
|
195
|
+
return classify_upstream(400, raw)
|
|
196
|
+
if "rate limit" in low:
|
|
197
|
+
return classify_upstream(429, raw)
|
|
198
|
+
if "credit" in low:
|
|
199
|
+
return classify_upstream(402, raw)
|
|
200
|
+
if "api key" in low or "unauthorized" in low:
|
|
201
|
+
return classify_upstream(401, raw)
|
|
202
|
+
return playbook_entry("unknown_provider_error", detail=raw[:200])
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def notice_payload(entry: dict[str, Any]) -> dict[str, Any]:
|
|
206
|
+
"""SSE/JSON-safe dict for UI."""
|
|
207
|
+
out: dict[str, Any] = {
|
|
208
|
+
"code": str(entry.get("code") or "unknown_provider_error"),
|
|
209
|
+
"message": str(entry.get("message") or ""),
|
|
210
|
+
"hint": str(entry.get("hint") or ""),
|
|
211
|
+
"text": str(entry.get("message") or ""),
|
|
212
|
+
"error": str(entry.get("message") or ""),
|
|
213
|
+
}
|
|
214
|
+
action = entry.get("action")
|
|
215
|
+
if isinstance(action, dict):
|
|
216
|
+
out["action"] = action
|
|
217
|
+
label = entry.get("action_label")
|
|
218
|
+
if label:
|
|
219
|
+
out["action_label"] = str(label)
|
|
220
|
+
sec = entry.get("secondary_action")
|
|
221
|
+
if isinstance(sec, dict):
|
|
222
|
+
out["secondary_action"] = sec
|
|
223
|
+
sec_label = entry.get("secondary_action_label")
|
|
224
|
+
if sec_label:
|
|
225
|
+
out["secondary_action_label"] = str(sec_label)
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
e = classify_upstream(404, b'{"error":{"message":"No endpoints found for openrouter/owl-alpha."}}', model="openrouter/owl-alpha")
|
|
231
|
+
assert e["code"] == "model_unavailable"
|
|
232
|
+
e2 = classify_exception_text("no_llm_provider_for_user: Connect an LLM provider")
|
|
233
|
+
assert e2["code"] == "no_llm_provider"
|
|
234
|
+
e3 = classify_upstream(401, b'{"error":"Invalid API key"}')
|
|
235
|
+
assert e3["code"] == "provider_invalid_key"
|
|
236
|
+
print("llm_error_glossary ok")
|
|
@@ -144,6 +144,9 @@ def _build_upstream_request(
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
upstream_headers.update(upstream_auth_header(provider, secret))
|
|
147
|
+
if provider == "openrouter":
|
|
148
|
+
upstream_headers.setdefault("HTTP-Referer", "https://workfra.me")
|
|
149
|
+
upstream_headers.setdefault("X-Title", "Workframe")
|
|
147
150
|
|
|
148
151
|
req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
|
|
149
152
|
return req, None
|